diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8861a03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Build artifacts and dependencies +node_modules/ +vendor/ +target/ +dist/ +build/ +bin/ +obj/ + +# Go module cache +/root/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Environment and keys +.env +*.pem +!.env.example + +# Logs +*.log diff --git a/README.md b/README.md index 9a5de7b..2607d8c 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ -# age-scan-examples \ No newline at end of file +# age-scan-examples + +This repository contains examples for integrating with Yoti AI Services API in multiple programming languages. + +## API Overview + +The examples use the Yoti AI Services API with the following structure: + +- **Base URL**: `https://api.yoti.com/ai/v1` +- **Available Endpoints**: + - `/age` - Age estimation only + - `/antispoofing` - Liveness/antispoofing check only + - `/age-antispoofing` - Combined age estimation and antispoofing check (default) + +## Available Examples + +- [Python](./python/README.md) +- [JavaScript/Node.js](./javascript/README.md) +- [Java](./java/README.md) +- [Go](./go/README.md) +- [.NET Core](./dotnet/CoreExample/README.md) +- [PHP](./php/README.md) + +## Getting Started + +Each example requires: + +1. A Yoti SDK ID and PEM file (obtain from [Yoti Hub](https://hub.yoti.com)) +2. Configuration of environment variables or properties file +3. An image file for testing (provided as `testimage.jpg` or `image.jpeg`) + +See individual example READMEs for language-specific setup instructions. + +## Reference + +For more information, see: +- [Yoti AI Services API Documentation](https://developers.yoti.com/ai-services-api) +- [Web FCM Demo](https://github.com/getyoti/web-fcm-demo) \ No newline at end of file diff --git a/dotnet/CoreExample/CoreExample/.env b/dotnet/CoreExample/CoreExample/.env index 39d23ce..5ec82e5 100644 --- a/dotnet/CoreExample/CoreExample/.env +++ b/dotnet/CoreExample/CoreExample/.env @@ -1,5 +1,6 @@ TEST_IMAGE_PATH = "testimage.jpg" -BASE_URL = "https://api.yoti.com" +BASE_URL = "https://api.yoti.com/ai/v1" +ENDPOINT = "age-antispoofing" PEM_FILE_PATH = "keys/key.pem" SDK_ID = "" diff --git a/dotnet/CoreExample/CoreExample/Program.cs b/dotnet/CoreExample/CoreExample/Program.cs index 10921c0..ab35931 100644 --- a/dotnet/CoreExample/CoreExample/Program.cs +++ b/dotnet/CoreExample/CoreExample/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Yoti.Auth.Web; using System; @@ -8,13 +8,13 @@ using Org.BouncyCastle.Crypto; using System.Text; -namespace CoreExample -{ - - class Program - { - static void Main(string[] args) - { +namespace CoreExample +{ + + class Program + { + static void Main(string[] args) + { var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -46,14 +46,14 @@ static void Main(string[] args) string serializedRequest = Newtonsoft.Json.JsonConvert.SerializeObject(new { - data = Convert.ToBase64String(imgBytes) + img = Convert.ToBase64String(imgBytes) }); byte[] byteContent = Encoding.UTF8.GetBytes(serializedRequest); Request request = new RequestBuilder() - .WithBaseUri(new Uri(DotNetEnv.Env.GetString("BASE_URL") + "/api/v1/age-verification")) - .WithEndpoint("/checks") + .WithBaseUri(new Uri(DotNetEnv.Env.GetString("BASE_URL"))) + .WithEndpoint("/" + DotNetEnv.Env.GetString("ENDPOINT")) .WithHttpMethod(HttpMethod.Post) .WithKeyPair(key) .WithHeader("X-Yoti-Auth-Id", DotNetEnv.Env.GetString("SDK_ID")) @@ -78,9 +78,9 @@ private static void ConfigureServices(IServiceCollection services) } - - } -} - - - + + } +} + + + diff --git a/dotnet/CoreExample/README.md b/dotnet/CoreExample/README.md index bbfcc8c..37052c3 100644 --- a/dotnet/CoreExample/README.md +++ b/dotnet/CoreExample/README.md @@ -1,6 +1,7 @@ # .NET Core Example - Save your PEM file into the keys directory and name it key.pem -- Edit the `.env` file, adding client SDK ID and BASE_URL +- Edit the `.env` file, adding client SDK ID +- Optional - Update the ENDPOINT in `.env` (default is `age-antispoofing`, can be `age`, `antispoofing`, or `age-antispoofing`) - Optional - Replace the `testimage.jpg` with your own. - Run the project with `dotnet run -p CoreExample.csproj` diff --git a/go/.env.example b/go/.env.example index 39d23ce..5ec82e5 100644 --- a/go/.env.example +++ b/go/.env.example @@ -1,5 +1,6 @@ TEST_IMAGE_PATH = "testimage.jpg" -BASE_URL = "https://api.yoti.com" +BASE_URL = "https://api.yoti.com/ai/v1" +ENDPOINT = "age-antispoofing" PEM_FILE_PATH = "keys/key.pem" SDK_ID = "" diff --git a/go/README.md b/go/README.md index 43115e2..a2f15ba 100644 --- a/go/README.md +++ b/go/README.md @@ -1,6 +1,7 @@ # Golang Example - Save your PEM file into the keys directory and name it key.pem -- Edit the `.env` file, adding client SDK ID and BASE_URL +- Copy `.env.example` to `.env` and add your client SDK ID +- Optional - Update the ENDPOINT in `.env` (default is `age-antispoofing`, can be `age`, `antispoofing`, or `age-antispoofing`) - Optional - Replace the `testimage.jpg` with your own. - Run the project with `go run main.go` diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..4b1eafb --- /dev/null +++ b/go/go.mod @@ -0,0 +1,8 @@ +module github.com/getyoti/age-scan-examples/go + +go 1.24.4 + +require ( + github.com/getyoti/yoti-go-sdk/v3 v3.15.0 + github.com/joho/godotenv v1.5.1 +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..be30284 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,8 @@ +github.com/getyoti/yoti-go-sdk/v3 v3.15.0 h1:jLbL6gGvNXUYaI8GHQ3S7uLoPk/S6SMeWaHI4TtSIFo= +github.com/getyoti/yoti-go-sdk/v3 v3.15.0/go.mod h1:FH8g7mRttc6SBUd9P0Jihm7ut0rNhkU3rDFljUHL33I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/go/main.go b/go/main.go index 8577e17..5803ae9 100644 --- a/go/main.go +++ b/go/main.go @@ -15,36 +15,51 @@ import ( ) type Estimation struct { - Data string `json:"data"` + Img string `json:"img"` } func main(){ sdkID := os.Getenv("SDK_ID") baseURL := os.Getenv("BASE_URL") + endpoint := os.Getenv("ENDPOINT") keyFile := os.Getenv("PEM_FILE_PATH") imgPath := os.Getenv("TEST_IMAGE_PATH") - file, _ := os.Open(imgPath) + file, err := os.Open(imgPath) + if err != nil { + fmt.Println(err) + return + } + defer file.Close() reader := bufio.NewReader(file) - content, _ := ioutil.ReadAll(reader) + content, err := ioutil.ReadAll(reader) + if err != nil { + fmt.Println(err) + return + } encoded := base64.StdEncoding.EncodeToString(content) estimation := &Estimation{ - Data:encoded, + Img: encoded, } - jsonData,err := json.Marshal(estimation) + jsonData, err := json.Marshal(estimation) if err != nil { fmt.Println(err) + return } - key, _ := ioutil.ReadFile(keyFile) + key, err := ioutil.ReadFile(keyFile) + if err != nil { + fmt.Println(err) + return + } // Create request - req,_ := requests.SignedRequest{ + req, err := requests.SignedRequest{ HTTPMethod: http.MethodPost, - BaseURL: baseURL + "/api/v1/age-verification", - Endpoint: "/checks", + BaseURL: baseURL, + Endpoint: "/" + endpoint, Headers: map[string][]string{ "Content-Type": {"application/json"}, "Accept": {"application/json"}, @@ -52,9 +67,17 @@ func main(){ }, Body: jsonData, }.WithPemFile(key).Request() + if err != nil { + fmt.Println(err) + return + } //get Yoti response - response, _ := http.DefaultClient.Do(req) + response, err := http.DefaultClient.Do(req) + if err != nil { + fmt.Println(err) + return + } buffer := new(strings.Builder) _, err = io.Copy(buffer, response.Body) diff --git a/java/README.md b/java/README.md index f0bcff4..6431636 100644 --- a/java/README.md +++ b/java/README.md @@ -1,7 +1,8 @@ # Java Example -- Save your PEM file into the keys directory and name it key.pem -- Edit the `application.properties` file, adding client SDK ID and BASE_URL +- Save your PEM file into the `resources/keys` directory and name it key.pem +- Edit the `application.properties` file, adding client SDK ID +- Optional - Update the ENDPOINT in `application.properties` (default is `age-antispoofing`, can be `age`, `antispoofing`, or `age-antispoofing`) - Optional - Replace the testimage.jpg with your own. - Build the project `mvn clean package` - Run the example `java -jar target/age-scan-1.0.jar` diff --git a/java/pom.xml b/java/pom.xml index e7b1c6f..b2e3ac2 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.yoti @@ -18,6 +18,45 @@ + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + true + com.yoti.agescan.Application + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + org.apache.maven.plugins maven-deploy-plugin @@ -46,4 +85,4 @@ - + \ No newline at end of file diff --git a/java/src/main/java/com/yoti/agescan/Application.java b/java/src/main/java/com/yoti/agescan/Application.java index 4542c02..6db9d40 100644 --- a/java/src/main/java/com/yoti/agescan/Application.java +++ b/java/src/main/java/com/yoti/agescan/Application.java @@ -35,18 +35,22 @@ public static void main(String[] args) { BufferedImage bufferedImage = null; ByteArrayOutputStream byteArrayOutputStream = null; try { - bufferedImage = ImageIO.read(new File(Application.class.getClassLoader(). - getResource(prop.getProperty("TEST_IMAGE_PATH")). - getFile())); + InputStream imageStream = Application.class.getClassLoader() + .getResourceAsStream(prop.getProperty("TEST_IMAGE_PATH")); + if (imageStream == null) { + System.out.println("Image not found in resources!"); + return; + } + bufferedImage = ImageIO.read(imageStream); byteArrayOutputStream = new ByteArrayOutputStream(); - ImageIO.write(bufferedImage, "jpg", byteArrayOutputStream ); + ImageIO.write(bufferedImage, "jpg", byteArrayOutputStream); } catch (IOException e) { e.printStackTrace(); } byte[] image = byteArrayOutputStream.toByteArray(); JsonObject body = new JsonObject(); - body.put("data", image); + body.put("img", image); byte[] payload = body.encode().getBytes(); @@ -54,8 +58,8 @@ public static void main(String[] args) { try { SignedRequest signedRequest = SignedRequestBuilder.newInstance() .withKeyPair(findKeyPair()) - .withBaseUrl(prop.getProperty("HOST") + "/api/v1/age-verification") - .withEndpoint("/checks") + .withBaseUrl(prop.getProperty("BASE_URL")) + .withEndpoint("/" + prop.getProperty("ENDPOINT")) .withPayload(payload) .withHttpMethod("POST") .withHeader("X-Yoti-Auth-Id", prop.getProperty("SDK_ID")) @@ -74,8 +78,11 @@ public static void main(String[] args) { * @throws IOException */ private static KeyPair findKeyPair() throws IOException { - InputStream keyStream = new FileInputStream(Application.class.getClassLoader().getResource(prop.getProperty("PEM_FILE_PATH")).getFile()); - PEMParser reader = new PEMParser(new BufferedReader(new InputStreamReader(keyStream, Charset.defaultCharset()))); + InputStream keyStream = Application.class.getClassLoader() + .getResourceAsStream(prop.getProperty("PEM_FILE_PATH")); + if (keyStream == null) { + throw new FileNotFoundException("PEM key file not found in resources!"); + } PEMParser reader = new PEMParser(new BufferedReader(new InputStreamReader(keyStream, Charset.defaultCharset()))); KeyPair keyPair = null; for (Object o = null; (o = reader.readObject()) != null;) { if (o instanceof PEMKeyPair) { diff --git a/java/src/main/resources/application.properties b/java/src/main/resources/application.properties index d7e7039..f5811fe 100644 --- a/java/src/main/resources/application.properties +++ b/java/src/main/resources/application.properties @@ -1,4 +1,5 @@ TEST_IMAGE_PATH = testimage.jpg -HOST = +BASE_URL = https://api.yoti.com/ai/v1 +ENDPOINT = age-antispoofing PEM_FILE_PATH = keys/key.pem SDK_ID = diff --git a/javascript/.env b/javascript/.env index d5b8bdc..5ec82e5 100644 --- a/javascript/.env +++ b/javascript/.env @@ -1,5 +1,6 @@ TEST_IMAGE_PATH = "testimage.jpg" -BASE_URL = "" +BASE_URL = "https://api.yoti.com/ai/v1" +ENDPOINT = "age-antispoofing" PEM_FILE_PATH = "keys/key.pem" SDK_ID = "" diff --git a/javascript/README.md b/javascript/README.md index adda743..91efdaa 100644 --- a/javascript/README.md +++ b/javascript/README.md @@ -2,6 +2,7 @@ - Run `npm install` - Save your PEM file into the keys directory and name it key.pem -- Edit the `.env` file, adding client SDK ID and BASE_URL +- Edit the `.env` file, adding client SDK ID +- Optional - Update the ENDPOINT in `.env` (default is `age-antispoofing`, can be `age`, `antispoofing`, or `age-antispoofing`) - Optional - Replace the `testimage.jpg` with your own. - Run the project with `npm start` diff --git a/javascript/index.js b/javascript/index.js index 27ae729..6e99b25 100644 --- a/javascript/index.js +++ b/javascript/index.js @@ -4,12 +4,12 @@ const { RequestBuilder, Payload } = require('yoti'); const fs = require('fs') var image = fs.readFileSync(process.env.TEST_IMAGE_PATH); -var imageRequest = {"data": image.toString('base64')} +var imageRequest = {"img": image.toString('base64')} const request = new RequestBuilder() - .withBaseUrl(process.env.BASE_URL + '/api/v1/age-verification') + .withBaseUrl(process.env.BASE_URL) .withPemFilePath(process.env.PEM_FILE_PATH) - .withEndpoint('/checks') + .withEndpoint('/' + process.env.ENDPOINT) .withPayload(new Payload(imageRequest)) .withMethod('POST') .withHeader('X-Yoti-Auth-Id', process.env.SDK_ID) diff --git a/php/README.md b/php/README.md new file mode 100644 index 0000000..c293951 --- /dev/null +++ b/php/README.md @@ -0,0 +1,12 @@ +# PHP Example + +- Run `composer install` +- Save your PEM file into the same directory and name it key.pem +- Set environment variables or edit the `index.php` file: + - `SDK_ID`: Your client SDK ID (required) + - `BASE_URL`: API base URL (default: `https://api.yoti.com/ai/v1`) + - `ENDPOINT`: API endpoint (default: `age-antispoofing`, can be `age`, `antispoofing`, or `age-antispoofing`) + - `PEM_FILE_PATH`: Path to PEM file (default: `key.pem`) + - `IMAGE_PATH`: Path to test image (default: `./image.jpeg`) +- Optional - Replace the `image.jpeg` with your own. +- Run the project with `php index.php` diff --git a/php/index.php b/php/index.php index 768a645..44fedae 100644 --- a/php/index.php +++ b/php/index.php @@ -5,17 +5,31 @@ use Yoti\Http\RequestBuilder; use Yoti\Http\Payload; -$image = file_get_contents('./image.jpeg'); +// Configuration - update these values or set environment variables +$baseUrl = getenv('BASE_URL') ?: 'https://api.yoti.com/ai/v1'; +$endpoint = getenv('ENDPOINT') ?: 'age-antispoofing'; +$pemFilePath = getenv('PEM_FILE_PATH') ?: 'key.pem'; +$sdkId = getenv('SDK_ID'); +$imagePath = getenv('IMAGE_PATH') ?: './image.jpeg'; -$payload = [ "data" => base64_encode($image) ]; +if (!$sdkId) { + die("Error: SDK_ID environment variable is required. Please set SDK_ID or update the code directly.\n"); +} + +$image = file_get_contents($imagePath); +if ($image === false) { + die("Error: Could not read image file: $imagePath\n"); +} + +$payload = [ "img" => base64_encode($image) ]; $request = (new RequestBuilder()) - ->withBaseUrl('/api/v1/age-verification') - ->withPemFilePath('key.pem') - ->withEndpoint('/checks') + ->withBaseUrl($baseUrl) + ->withPemFilePath($pemFilePath) + ->withEndpoint('/' . $endpoint) ->withMethod('POST') ->withPayload(Payload::fromJsonData($payload)) - ->withHeader('X-Yoti-Auth-Id', '') + ->withHeader('X-Yoti-Auth-Id', $sdkId) ->build(); $response = $request->execute(); diff --git a/python/.env b/python/.env index d5b8bdc..5ec82e5 100644 --- a/python/.env +++ b/python/.env @@ -1,5 +1,6 @@ TEST_IMAGE_PATH = "testimage.jpg" -BASE_URL = "" +BASE_URL = "https://api.yoti.com/ai/v1" +ENDPOINT = "age-antispoofing" PEM_FILE_PATH = "keys/key.pem" SDK_ID = "" diff --git a/python/README.md b/python/README.md index 4cf2ce4..0a2aa1c 100644 --- a/python/README.md +++ b/python/README.md @@ -1,6 +1,7 @@ # Python Example - Save your PEM file into the keys directory and name it key.pem -- Edit the `.env` file, adding client SDK ID and BASE_URL +- Edit the `.env` file, adding client SDK ID +- Optional - Update the ENDPOINT in `.env` (default is `age-antispoofing`, can be `age`, `antispoofing`, or `age-antispoofing`) - Optional - Replace the testimage.jpg with your own. - Run the example `python example.py` diff --git a/python/example.py b/python/example.py index ccf079b..5cbb310 100644 --- a/python/example.py +++ b/python/example.py @@ -13,9 +13,11 @@ def execute(request): return response.content def generate_session(): + endpoint = os.getenv('ENDPOINT', 'age-antispoofing') + with open(os.getenv('TEST_IMAGE_PATH'), "rb") as image_file: encoded_string = base64.b64encode(image_file.read()) - data = {"data" : encoded_string.decode("utf-8")} + data = {"img" : encoded_string.decode("utf-8")} payload_string = json.dumps(data).encode() @@ -23,8 +25,8 @@ def generate_session(): SignedRequest .builder() .with_pem_file(os.getenv('PEM_FILE_PATH')) - .with_base_url(os.getenv('HOST') + "/api/v1/age-verification") - .with_endpoint("/checks") + .with_base_url(os.getenv('BASE_URL')) + .with_endpoint("/" + endpoint) .with_http_method("POST") .with_header("X-Yoti-Auth-Id", os.getenv('SDK_ID')) .with_payload(payload_string) @@ -32,7 +34,7 @@ def generate_session(): ) - # get Yoti response + # get Yoti response response = signed_request.execute() response_payload = json.loads(response.text) print(response_payload) diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/list b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/list new file mode 100644 index 0000000..f77833d --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/list @@ -0,0 +1,2 @@ +v1.1.0 +v2.3.1+incompatible diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v1.1.0.mod b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v1.1.0.mod new file mode 100644 index 0000000..53d9407 --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v1.1.0.mod @@ -0,0 +1 @@ +module github.com/getyoti/yoti-go-sdk diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.info b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.info new file mode 100644 index 0000000..49483d5 --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.info @@ -0,0 +1 @@ +{"Version":"v2.3.1+incompatible","Time":"2019-02-28T12:31:44Z"} \ No newline at end of file diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.lock b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.lock new file mode 100644 index 0000000..e69de29 diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.mod b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.mod new file mode 100644 index 0000000..53d9407 --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.mod @@ -0,0 +1 @@ +module github.com/getyoti/yoti-go-sdk diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.zip b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.zip new file mode 100644 index 0000000..593830a Binary files /dev/null and b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.zip differ diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.ziphash b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.ziphash new file mode 100644 index 0000000..ae8337a --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/@v/v2.3.1+incompatible.ziphash @@ -0,0 +1 @@ +h1:+hiLmgo7aT1q1psL5ypahU4jHeUfWnIjF9t4CgJQSes= \ No newline at end of file diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/list b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/list new file mode 100644 index 0000000..5ef3202 --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/list @@ -0,0 +1 @@ +v3.14.0 diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.info b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.info new file mode 100644 index 0000000..56a19a6 --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.info @@ -0,0 +1 @@ +{"Version":"v3.14.0","Time":"2025-06-12T08:13:54Z","Origin":{"VCS":"git","URL":"https://github.com/getyoti/yoti-go-sdk","Hash":"84a18a5bb476a96652219fa7bfbe68f037781a54","Ref":"refs/tags/v3.14.0"}} \ No newline at end of file diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.lock b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.lock new file mode 100644 index 0000000..e69de29 diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.mod b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.mod new file mode 100644 index 0000000..c1c87ab --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.mod @@ -0,0 +1,10 @@ +module github.com/getyoti/yoti-go-sdk/v3 + +require ( + google.golang.org/protobuf v1.28.0 + gotest.tools/v3 v3.3.0 +) + +require github.com/google/go-cmp v0.5.5 // indirect + +go 1.19 diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.zip b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.zip new file mode 100644 index 0000000..0a45b99 Binary files /dev/null and b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.zip differ diff --git a/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.ziphash b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.ziphash new file mode 100644 index 0000000..1e3ca7a --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/getyoti/yoti-go-sdk/v3/@v/v3.14.0.ziphash @@ -0,0 +1 @@ +h1:cMFC/PuN6kuxMfPwX4OkVyQDA5ZousWnVMfUhIhKZa0= \ No newline at end of file diff --git a/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/list b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/list new file mode 100644 index 0000000..12aa8c5 --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/list @@ -0,0 +1 @@ +v0.5.5 diff --git a/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.lock b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.lock new file mode 100644 index 0000000..e69de29 diff --git a/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.mod b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.mod new file mode 100644 index 0000000..5391dee --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.mod @@ -0,0 +1,5 @@ +module github.com/google/go-cmp + +go 1.8 + +require golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 diff --git a/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.zip b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.zip new file mode 100644 index 0000000..40a8595 Binary files /dev/null and b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.zip differ diff --git a/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.ziphash b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.ziphash new file mode 100644 index 0000000..bb137ae --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/google/go-cmp/@v/v0.5.5.ziphash @@ -0,0 +1 @@ +h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= \ No newline at end of file diff --git a/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/list b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/list new file mode 100644 index 0000000..53b5bbb --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/list @@ -0,0 +1 @@ +v1.5.1 diff --git a/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.info b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.info new file mode 100644 index 0000000..c8569b6 --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.info @@ -0,0 +1 @@ +{"Version":"v1.5.1","Time":"2023-02-05T21:47:38Z","Origin":{"VCS":"git","URL":"https://github.com/joho/godotenv","Hash":"3fc4292b58a67b78e1dbb6e47b4879a6cc602ec4","Ref":"refs/tags/v1.5.1"}} \ No newline at end of file diff --git a/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.lock b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.lock new file mode 100644 index 0000000..e69de29 diff --git a/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.mod b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.mod new file mode 100644 index 0000000..126e61d --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.mod @@ -0,0 +1,3 @@ +module github.com/joho/godotenv + +go 1.12 diff --git a/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.zip b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.zip new file mode 100644 index 0000000..e1e42d2 Binary files /dev/null and b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.zip differ diff --git a/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.ziphash b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.ziphash new file mode 100644 index 0000000..6ab2d4a --- /dev/null +++ b/root/pkg/mod/cache/download/github.com/joho/godotenv/@v/v1.5.1.ziphash @@ -0,0 +1 @@ +h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= \ No newline at end of file diff --git a/root/pkg/mod/cache/download/gotest.tools/v3/@v/list b/root/pkg/mod/cache/download/gotest.tools/v3/@v/list new file mode 100644 index 0000000..b299be9 --- /dev/null +++ b/root/pkg/mod/cache/download/gotest.tools/v3/@v/list @@ -0,0 +1 @@ +v3.3.0 diff --git a/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.lock b/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.lock new file mode 100644 index 0000000..e69de29 diff --git a/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.mod b/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.mod new file mode 100644 index 0000000..abeca34 --- /dev/null +++ b/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.mod @@ -0,0 +1,10 @@ +module gotest.tools/v3 + +go 1.13 + +require ( + github.com/google/go-cmp v0.5.5 + github.com/spf13/pflag v1.0.3 + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 + golang.org/x/tools v0.1.0 +) diff --git a/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.zip b/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.zip new file mode 100644 index 0000000..60831dc Binary files /dev/null and b/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.zip differ diff --git a/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.ziphash b/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.ziphash new file mode 100644 index 0000000..e0c99fb --- /dev/null +++ b/root/pkg/mod/cache/download/gotest.tools/v3/@v/v3.3.0.ziphash @@ -0,0 +1 @@ +h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= \ No newline at end of file diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk/v3@v3.14.0 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk/v3@v3.14.0 new file mode 100644 index 0000000..79ba94e --- /dev/null +++ b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk/v3@v3.14.0 @@ -0,0 +1,9 @@ +38859053 +github.com/getyoti/yoti-go-sdk/v3 v3.14.0 h1:cMFC/PuN6kuxMfPwX4OkVyQDA5ZousWnVMfUhIhKZa0= +github.com/getyoti/yoti-go-sdk/v3 v3.14.0/go.mod h1:FH8g7mRttc6SBUd9P0Jihm7ut0rNhkU3rDFljUHL33I= + +go.sum database tree +46687137 +AThGHJvQbj2AItv5E753W1Rtkno5LM7juWi9QkFPmOo= + +— sum.golang.org Az3grjiLRqz6DECL67zEs0IdbJ0k1TjmcbMkUj4pjwIgQqQ5i/JXxTDISHmldTEpiNheRMxEKdlomajjGk0nkaSZCAE= diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk@v1.1.0 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk@v1.1.0 new file mode 100644 index 0000000..c0d1702 --- /dev/null +++ b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk@v1.1.0 @@ -0,0 +1,9 @@ +261090 +github.com/getyoti/yoti-go-sdk v1.1.0 h1:gL8+9+EJc+4rg0RbChTyYzFzSK9bmLRAvylZ0ZwAyzU= +github.com/getyoti/yoti-go-sdk v1.1.0/go.mod h1:q+DK9jO6Ir6nyXmIYH6wwrWq47ibBqK62fhgLe4kqzo= + +go.sum database tree +46687924 +oqyj00KFhQ1r/LMLYjS+N2ZcyMBmuy4E20Qx5wubH18= + +— sum.golang.org Az3grskfHYkRAlx6xDdD0Ld7PWz6CLORxgtPBxvd73HO8lvyC+Z2yYTTobhK5XRat+vJH6yqO8Oqxa4++MKlzqCl/QY= diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible new file mode 100644 index 0000000..2a14c9b --- /dev/null +++ b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible @@ -0,0 +1,9 @@ +261089 +github.com/getyoti/yoti-go-sdk v2.3.1+incompatible h1:+hiLmgo7aT1q1psL5ypahU4jHeUfWnIjF9t4CgJQSes= +github.com/getyoti/yoti-go-sdk v2.3.1+incompatible/go.mod h1:q+DK9jO6Ir6nyXmIYH6wwrWq47ibBqK62fhgLe4kqzo= + +go.sum database tree +46687779 +pxNpO4dXPoJtrPRtCMq8KxkiSi5F3cX8pifl+r4WP7U= + +— sum.golang.org Az3grs3KohpAvBeFkfu4CQTg0VEAMa69PaRcHfZfZyjG6f3qU3VgYStt+C9zFqWMegNhXyhsQCqGznH7Utnk5n4rGAM= diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/google/go-cmp@v0.5.5 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/google/go-cmp@v0.5.5 new file mode 100644 index 0000000..cb9afd3 --- /dev/null +++ b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/google/go-cmp@v0.5.5 @@ -0,0 +1,9 @@ +3171667 +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= + +go.sum database tree +46686987 +Z8Xci6st06/WRnp3Fiiyllutio2B545XhO148PaLqn8= + +— sum.golang.org Az3grgqT8XzQNWm0rf0clpbxNQUse9V9F5TeZTfBJlNcEFg3FHwPTA97p6A2JSjDFmptr6XJTtPx/EgYK8eYyP46EAo= diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/joho/godotenv@v1.5.1 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/joho/godotenv@v1.5.1 new file mode 100644 index 0000000..2e12426 --- /dev/null +++ b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/github.com/joho/godotenv@v1.5.1 @@ -0,0 +1,9 @@ +15449903 +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= + +go.sum database tree +46684670 +/AOfVNHvKwrfPFvkTxwDG7szue3hZXrsXDNPQN3oFdA= + +— sum.golang.org Az3grrWTXFuqO1ViDo8WBY4UXIneoik9d5BGNcm5xN8W1I1tP8GizQ0lNQVfztfznkxvfVF0cI4emlQvI6Uqim3Y5AQ= diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/gotest.tools/v3@v3.3.0 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/gotest.tools/v3@v3.3.0 new file mode 100644 index 0000000..111c3b7 --- /dev/null +++ b/root/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/gotest.tools/v3@v3.3.0 @@ -0,0 +1,9 @@ +11035727 +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= + +go.sum database tree +46684715 +dtVIxIDuMQ5vISP9oB3wTEU0L4aDEKzeEtAlFI2eR1I= + +— sum.golang.org Az3grg+g4o0B7qWCziJOy+0L8wM2Z1ijdrokO2RnAiJrMO/5ccjMwd+E3VrnEgsmNWnmNr0o+lGylSp9ySIqtwOTFws= diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x001/019 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x001/019 new file mode 100644 index 0000000..1d607c2 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x001/019 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x012/389 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x012/389 new file mode 100644 index 0000000..821ac72 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x012/389 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x043/108 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x043/108 new file mode 100644 index 0000000..f248ade Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x043/108 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x060/351 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x060/351 new file mode 100644 index 0000000..831cb04 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x060/351 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x151/793 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x151/793 new file mode 100644 index 0000000..94d1254 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x151/793 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/361 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/361 new file mode 100644 index 0000000..c8d85bc Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/361 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/361.p/254 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/361.p/254 new file mode 100644 index 0000000..9070d3b Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/361.p/254 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/362 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/362 new file mode 100644 index 0000000..fb46c72 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/362 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/371 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/371 new file mode 100644 index 0000000..66da9da Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/371 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/371.p/161 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/371.p/161 new file mode 100644 index 0000000..4d51cb9 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/371.p/161 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/374.p/180 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/374.p/180 new file mode 100644 index 0000000..0d252a0 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x182/374.p/180 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/003 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/003 new file mode 100644 index 0000000..264d451 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/003 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/048 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/048 new file mode 100644 index 0000000..ab9e346 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/048 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/168 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/168 new file mode 100644 index 0000000..b040735 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/168 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/235 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/235 new file mode 100644 index 0000000..a24031d Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/235 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/592 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/592 new file mode 100644 index 0000000..f7b2085 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/592 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/102 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/102 new file mode 100644 index 0000000..88736e4 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/102 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/89 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/89 new file mode 100644 index 0000000..6d2372f Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/89 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/99 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/99 new file mode 100644 index 0000000..9c84719 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/712.p/99 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/000 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/000 new file mode 100644 index 0000000..a72d7b3 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/000 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/002.p/200 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/002.p/200 new file mode 100644 index 0000000..c216725 Binary files /dev/null and b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/002.p/200 differ diff --git a/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/3/000.p/2 b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/3/000.p/2 new file mode 100644 index 0000000..948d7fa --- /dev/null +++ b/root/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/3/000.p/2 @@ -0,0 +1,3 @@ +o +@m%qT `ow!^Z{ˍv51 "sJYF  +i9Z9 \ No newline at end of file diff --git a/root/pkg/mod/cache/lock b/root/pkg/mod/cache/lock new file mode 100644 index 0000000..e69de29 diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.gitattributes b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/ISSUE_TEMPLATE.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..98f0c89 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,17 @@ +--- +name: Custom issue template +about: " There's a better way to get help!" +title: '' +labels: '' +assignees: '' + +--- + +# +# Wait ✋ +# +# There's a better way to get help! +# +# Send your questions or issues to https://support.yoti.com +# +# diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/dependabot.yml b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/dependabot.yml new file mode 100644 index 0000000..2459bd1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 3 + target-branch: development + reviewers: + - getyoti/yoti-sign + assignees: + - getyoti/yoti-sign diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/codeql-analysis.yml b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..fae8c9c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/codeql-analysis.yml @@ -0,0 +1,57 @@ +name: "CodeQL" + +on: + push: + branches: [ master, development* ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master, development* ] + schedule: + - cron: '21 5 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/sonar.yaml b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/sonar.yaml new file mode 100644 index 0000000..d14ef3a --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/sonar.yaml @@ -0,0 +1,31 @@ +name: Sonar Scan +on: [push, pull_request_target] + +jobs: + sonar: + name: Sonar Scan + runs-on: ubuntu-latest + # always run on push events + # only run on pull_request_target event when pull request pulls from fork repository + if: > + github.event_name == 'push' || + github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v2 + with: + go-version: 1.18 + + - run: go test -json ./... > report.json + + - run: go test -coverprofile=coverage.out -json ./... > sonar-report.json + + - uses: sonarsource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/tests.yaml b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/tests.yaml new file mode 100644 index 0000000..658d363 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.github/workflows/tests.yaml @@ -0,0 +1,29 @@ +name: Unit Tests +on: [push, pull_request_target] + +jobs: + tests: + name: Tests (Go ${{ matrix.go-version }}) + runs-on: ubuntu-latest + # always run on push events + # only run on pull_request_target event when pull request pulls from fork repository + if: > + github.event_name == 'push' || + github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + strategy: + fail-fast: false + matrix: + go-version: [1.19, "^1"] + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - run: ./sh/gofmt.sh + + - run: go vet ./... + + - run: go test -v -race ./... diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.gitignore b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.gitignore new file mode 100644 index 0000000..a1fb98b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.gitignore @@ -0,0 +1,23 @@ +.vscode + +# Test binary, build with `go test -c` +*.test + +# Key generated in test +cryptoutil/tmpKey.pem + +# Debug files +debug + +# Report files +sonar-report.json +coverage.out +report.json + +# idea files +.idea + +# DS_Store files +.DS_Store + + diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.golangci.yaml b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.golangci.yaml new file mode 100644 index 0000000..6799a1f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.golangci.yaml @@ -0,0 +1,26 @@ +# .golangci.yaml +run: + timeout: 5m # Example: set a timeout + issues: + # Disable SA1019 directly or per linter if needed + # https://golangci-lint.run/usage/configuration/#issues-configuration + exclude-rules: + # Exclude all SA1019 issues globally + - text: "SA1019" # Match the error message part for SA1019 (e.g., "SA1019: example is deprecated") + linters: + - staticcheck # Or specific linter if SA1019 comes from multiple sources + + # Alternatively, if SA1019 is specific to certain paths: + # - text: "SA1019" + # path: "internal/deprecated_stuff/" # Exclude issues in specific directories + # linters: + # - staticcheck + +linters: + # Explicitly enable linters you want to use. + # If empty, default linters are used. + enable: + - staticcheck # Ensure staticcheck is enabled if you're trying to ignore its rules + # - govet + # - errcheck + # ... other linters you need diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.pre-commit-config.yaml b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.pre-commit-config.yaml new file mode 100644 index 0000000..35f7ab9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/.pre-commit-config.yaml @@ -0,0 +1,40 @@ +default_stages: + - commit +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: trailing-whitespace + - id: check-yaml + + - repo: local + hooks: + - id: local-go-fmt + name: 'go fmt' + entry: sh/gofmt.sh + language: 'script' + description: "Go Format" + + - id: local-go-build-modtidy + name: 'go build & go mod tidy' + entry: sh/go-build-modtidy.sh + language: 'script' + description: "Go build & go mod tidy" + + - id: local-go-imports + name: 'tidy imports' + entry: sh/goimports.sh + language: 'script' + description: "Go imports" + + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.3.5 + hooks: + - id: go-vet + exclude: _examples + - id: go-cyclo + exclude: _examples + args: [ "-over", "15", "-ignore", ".pb.go" ] + - id: go-unit-tests + args: ["-race", "./..."] + stages: [ push ] # only run tests on push, not on commit diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/CONTRIBUTING.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/CONTRIBUTING.md new file mode 100644 index 0000000..543aa55 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Contributing + +## Commit Process + +This repo comes with pre-commit hooks. These should be installed before commiting, done with: +```bash +pre-commit install --hook-type pre-commit --hook-type pre-push +``` +This will lint and run Go tools on commit, and run unit tests when pushing. diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/LICENSE.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/LICENSE.md new file mode 100644 index 0000000..fefdd34 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/LICENSE.md @@ -0,0 +1,23 @@ +# MIT License + +Copyright © 2017 Yoti Ltd + +* * * + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/README.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/README.md new file mode 100644 index 0000000..7a70c18 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/README.md @@ -0,0 +1,70 @@ +# Yoti Go SDK + +[![Build Status](https://github.com/getyoti/yoti-go-sdk/workflows/Unit%20Tests/badge.svg?branch=master)](https://github.com/getyoti/yoti-go-sdk/actions) +[![Go Report Card](https://goreportcard.com/badge/github.com/getyoti/yoti-go-sdk)](https://goreportcard.com/report/github.com/getyoti/yoti-go-sdk) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/getyoti/yoti-go-sdk/v3)](https://pkg.go.dev/github.com/getyoti/yoti-go-sdk/v3?tab=doc) +[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://github.com/getyoti/yoti-go-sdk/blob/master/LICENSE.md) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=getyoti%3Ago&metric=coverage)](https://sonarcloud.io/dashboard?id=getyoti%3Ago) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=getyoti%3Ago&metric=bugs)](https://sonarcloud.io/dashboard?id=getyoti%3Ago) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=getyoti%3Ago&metric=code_smells)](https://sonarcloud.io/dashboard?id=getyoti%3Ago) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=getyoti%3Ago&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=getyoti%3Ago) + +Welcome to the Yoti Go SDK. This repo contains the tools and step by step instructions you need to quickly integrate your Go back-end with Yoti so that your users can share their identity details with your application in a secure and trusted way. + +## Table of Contents + +1) [Requirements](#requirements) - +Requirements to use the SDK + +1) [Enabling the SDK](#enabling-the-sdk) - +How to add the SDK to your project + +1) [Setup](#setup) - +Setup required before using the Yoti services + +1) [Products](#products) - +Links to more information about the products offered by the Yoti SDK + +1) [Support](#support) - +Please feel free to reach out + +## Requirements + +Supported Go Versions: +- See [./.github/workflows/test.yaml](./.github/workflows/test.yaml) for supported versions + +## Enabling the SDK + +Simply add `github.com/getyoti/yoti-go-sdk/v3` as an import: +```Go +import "github.com/getyoti/yoti-go-sdk/v3" +``` +or add the following line to your go.mod file (check https://github.com/getyoti/yoti-go-sdk/releases for the latest version) +``` + +require github.com/getyoti/yoti-go-sdk/v3 v3.14.0 +``` + +## Setup + +For each service you will need: + +* Your Client SDK ID, generated by [Yoti Hub](https://hub.yoti.com) when you create (and then publish) your app. +* Your .pem file. This is your own unique private key which your browser generates from the [Yoti Hub](https://hub.yoti.com) when you create an application. + +## Products + +- [Yoti app integration](_docs/PROFILE.md) - Connect with already-verified customers. + - [Yoti app sandbox](_docs/PROFILE_SANDBOX.md) - Use the Sandbox SDK in conjunction with the Yoti app integration. + +## Support + +For any questions or support please contact us here: https://support.yoti.com +Please provide the following to get you up and working as quickly as possible: + +* Computer type +* OS version +* Version of Go being used +* Screenshot + +Once we have answered your question we may contact you again to discuss Yoti products and services. If you’d prefer us not to do this, please let us know when you e-mail. diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/IDV.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/IDV.md new file mode 100644 index 0000000..ba3b66c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/IDV.md @@ -0,0 +1,9 @@ + # Yoti IDV (Doc Scan) + + ## About + Yoti IDV can be seamlessly integrated with your website, app or custom product so you can perform secure identity checks. You'll be able to request specific ID documents from users directly from your website or app. + + See the [Developer Docs](https://developers.yoti.com/identity-verification/getting-started) for more information. + + ## Running the example +- See the [_examples/idv](../_examples/idv) folder for instructions on how to run the IDV Example project \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/IDV_SANDBOX.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/IDV_SANDBOX.md new file mode 100644 index 0000000..5de276f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/IDV_SANDBOX.md @@ -0,0 +1,25 @@ +# Yoti Go IDV (Doc Scan) Sandbox Module + +This module contains the tools you need to test your Go back-end integration with the IDV Sandbox service. + +## Importing the Sandbox + +You can reference the sandbox by adding the following import: + +```Go +import "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox" +``` + +## Configuration +The sandbox is initialised in the following way: +```Go +sandboxClient := sandbox.NewClient(sandboxClientSdkId, privateKey) +``` +* `sandboxClientSdkId` is the Sandbox SDK identifier generated from the Sandbox section on Yoti Hub. +* `privateKey` is the PEM file for your Sandbox application downloaded from the Yoti Hub, in the Sandbox section. + +Please do not open the PEM file, as this might corrupt the key, and you will need to redownload it. + +## Examples + +- [IDV Sandbox WebDriver Example](../_examples/docscansandbox/) \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/PROFILE.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/PROFILE.md new file mode 100644 index 0000000..76e418f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/PROFILE.md @@ -0,0 +1,153 @@ +Go Yoti App Integration +============================= + +1) [An Architectural View](#an-architectural-view) - +High level overview of integration + +1) [Profile Retrieval](#profile-retrieval) - +How to retrieve a Yoti profile using the one time use token + +1) [Running the example](#running-the-example) - +Running the profile example + +1) [API Coverage](#api-coverage) - +Attributes defined + +## An Architectural View + +To integrate your application with Yoti, your back-end must expose a GET endpoint that Yoti will use to forward tokens. +The endpoint can be configured in Yoti Hub when you create/update your application. + +The image below shows how your application back-end and Yoti integrate in the context of a Login flow. +Yoti SDK carries out for you steps 6, 7, 8, and the profile decryption in step 9. + +![alt text](login_flow.png "Login flow") + +## Profile Retrieval + +When your application receives a one time use token via the exposed endpoint (it will be assigned to a query string parameter named `token`), you can easily retrieve the activity details by adding the following to your endpoint handler: + +```Go +var activityDetails profile.ActivityDetails +activityDetails, err = client.GetActivityDetails(yotiOneTimeUseToken) +if err != nil { + // handle unhappy path +} +``` + +### Handling Errors +If a network error occurs that can be handled by resending the request, +the error returned by the SDK will implement the temporary error interface. +This can be tested for using a type assertion, and resent. + +```Go +var activityDetails profile.ActivityDetails +activityDetails, err = client.GetActivityDetails(yotiOneTimeUseToken) +tempError, temporary := err.(interface { + Temporary() bool +}) +if !temporary || !tempError.Temporary() { + // can retry same request +} +``` + +### Retrieving the user profile + +You can then get the user profile from the activityDetails struct: + +```Go +var rememberMeID string = activityDetails.RememberMeID() +var parentRememberMeID string = activityDetails.ParentRememberMeID() +var userProfile profile.UserProfile = activityDetails.UserProfile + +var selfie = userProfile.Selfie().Value() +var givenNames string = userProfile.GivenNames().Value() +var familyName string = userProfile.FamilyName().Value() +var fullName string = userProfile.FullName().Value() +var mobileNumber string = userProfile.MobileNumber().Value() +var emailAddress string = userProfile.EmailAddress().Value() +var address string = userProfile.Address().Value() +var gender string = userProfile.Gender().Value() +var nationality string = userProfile.Nationality().Value() +var dateOfBirth *time.Time +dobAttr, err := userProfile.DateOfBirth() +if err != nil { + // handle error +} else { + dateOfBirth = dobAttr.Value() +} +var structuredPostalAddress map[string]interface{} +structuredPostalAddressAttribute, err := userProfile.StructuredPostalAddress() +if err != nil { + // handle error +} else { + structuredPostalAddress := structuredPostalAddressAttribute.Value().(map[string]interface{}) +} +``` + +If you have chosen "Verify Condition" on the Yoti Hub with the age condition of "Over 18", you can retrieve the user information with the generic .GetAttribute method, which requires the result to be cast to the original type: + +```Go +userProfile.GetAttribute("age_over:18").Value().(string) +``` + +GetAttribute returns an interface, the value can be acquired through a type assertion. + +### Anchors, Sources and Verifiers + +An `Anchor` represents how a given Attribute has been _sourced_ or _verified_. These values are created and signed whenever a Profile Attribute is created, or verified with an external party. + +For example, an attribute value that was _sourced_ from a Passport might have the following values: + +`Anchor` property | Example value +-----|------ +type | SOURCE +value | PASSPORT +subType | OCR +signedTimestamp | 2017-10-31, 19:45:59.123789 + +Similarly, an attribute _verified_ against the data held by an external party will have an `Anchor` of type _VERIFIER_, naming the party that verified it. + +From each attribute you can retrieve the `Anchors`, and subsets `Sources` and `Verifiers` (all as `[]*anchor.Anchor`) as follows: + +```Go +givenNamesAnchors := userProfile.GivenNames().Anchors() +givenNamesSources := userProfile.GivenNames().Sources() +givenNamesVerifiers := userProfile.GivenNames().Verifiers() +``` + +You can also retrieve further properties from these respective anchors in the following way: + +```Go +var givenNamesFirstAnchor *anchor.Anchor = givenNamesAnchors[0] + +var anchorType anchor.Type = givenNamesFirstAnchor.Type() +var signedTimestamp *time.Time = givenNamesFirstAnchor.SignedTimestamp().Timestamp() +var subType string = givenNamesFirstAnchor.SubType() +var value string = givenNamesFirstAnchor.Value() +``` + +## Running the Example + +Follow the below link for instructions on how to run the example project: + +1) [Profile example](../_examples/profile/README.md) + +## API Coverage + +* [X] Activity Details + * [X] Remember Me ID `RememberMeID()` + * [X] Parent Remember Me ID `ParentRememberMeID()` + * [X] User Profile `UserProfile` + * [X] Selfie `Selfie()` + * [X] Selfie Base64 URL `Selfie().Value().Base64URL()` + * [X] Given Names `GivenNames()` + * [X] Family Name `FamilyName()` + * [X] Full Name `FullName()` + * [X] Mobile Number `MobileNumber()` + * [X] Email Address `EmailAddress()` + * [X] Date of Birth `DateOfBirth()` + * [X] Postal Address `Address()` + * [X] Structured Postal Address `StructuredPostalAddress()` + * [X] Gender `Gender()` + * [X] Nationality `Nationality()` \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/PROFILE_SANDBOX.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/PROFILE_SANDBOX.md new file mode 100644 index 0000000..ffe7777 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/PROFILE_SANDBOX.md @@ -0,0 +1,30 @@ +# Yoti Go Profile Sandbox Module + +This module contains the tools you need to test your Go back-end integration with the Yoti Sandbox service. + +## Importing the Sandbox + +You can reference the sandbox by adding the following import: + +```Go +import "github.com/getyoti/yoti-go-sdk/v3/profile/sandbox" +``` + +## Configuration +The sandbox is initialised in the following way: +```Go +sandboxClient := sandbox.Client{ + ClientSdkID: sandboxClientSdkId, + Key: privateKey, + } +``` +* `sandboxClientSdkId` is the Sandbox SDK identifier generated from the Sandbox section on Yoti Hub. +* `privateKey` is the PEM file for your Sandbox application downloaded from the Yoti Hub, in the Sandbox section. + +Please do not open the PEM file, as this might corrupt the key, and you will need to redownload it. + +The format of `privateKey` passed in to the client needs to be `*rsa.PrivateKey`. See the [sandboxexample_test.go](../_examples/profilesandbox/sandboxexample_test.go) to see how to easily create this struct. + +## Examples + +- [Profile Sandbox Example](../_examples/profilesandbox/) \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/login_flow.png b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/login_flow.png new file mode 100644 index 0000000..ca0e475 Binary files /dev/null and b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_docs/login_flow.png differ diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_examples/.gitignore b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_examples/.gitignore new file mode 100644 index 0000000..041d990 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/_examples/.gitignore @@ -0,0 +1,9 @@ +.env +# Generated binaries +docscan/docscan +idv/idv +aml/aml +docscansandbox/docscansandbox +profile/profile +profilesandbox/profilesandbox +digitalidentity/digitalidentity \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/aml.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/aml.go new file mode 100644 index 0000000..3245ca7 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/aml.go @@ -0,0 +1,38 @@ +package aml + +import ( + "encoding/json" +) + +// Address Address for Anti Money Laundering (AML) purposes +type Address struct { + Country string `json:"country"` + Postcode string `json:"post_code"` +} + +// Profile User profile for Anti Money Laundering (AML) checks +type Profile struct { + GivenNames string `json:"given_names"` + FamilyName string `json:"family_name"` + Address Address `json:"address"` + SSN string `json:"ssn"` +} + +// Result Result of Anti Money Laundering (AML) check for a particular user +type Result struct { + OnFraudList bool `json:"on_fraud_list"` + OnPEPList bool `json:"on_pep_list"` + OnWatchList bool `json:"on_watch_list"` +} + +// GetResult Parses AML result from response +func GetResult(response []byte) (Result, error) { + var result Result + err := json.Unmarshal(response, &result) + + if err != nil { + return result, err + } + + return result, err +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/service.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/service.go new file mode 100644 index 0000000..6860524 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/service.go @@ -0,0 +1,55 @@ +package aml + +import ( + "crypto/rsa" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/getyoti/yoti-go-sdk/v3/requests" +) + +func getCheckEndpoint(sdkID string) string { + return fmt.Sprintf("/aml-check?appId=%s", sdkID) +} + +// PerformCheck performs an Anti Money Laundering Check (AML) for a particular user. +// Returns three boolean values: 'OnPEPList', 'OnWatchList' and 'OnFraudList'. +func PerformCheck(httpClient requests.HttpClient, profile Profile, clientSdkId, apiUrl string, key *rsa.PrivateKey) (result Result, err error) { + payload, err := json.Marshal(profile) + if err != nil { + return + } + headers := requests.AuthKeyHeader(&key.PublicKey) + + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodPost, + BaseURL: apiUrl, + Endpoint: getCheckEndpoint(clientSdkId), + Headers: headers, + Body: payload, + }.Request() + if err != nil { + return + } + + httpErrorMessages := make(map[int]string) + httpErrorMessages[-1] = "AML Check was unsuccessful" + + var response *http.Response + response, err = requests.Execute(httpClient, request, httpErrorMessages) + if err != nil { + return result, err + } + + var responseBytes []byte + responseBytes, err = io.ReadAll(response.Body) + if err != nil { + return + } + + result, err = GetResult(responseBytes) + return +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/service_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/service_test.go new file mode 100644 index 0000000..d8ef5b0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/aml/service_test.go @@ -0,0 +1,155 @@ +package aml + +import ( + "crypto/rsa" + "errors" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "gotest.tools/v3/assert" +) + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +type mockReadCloser struct { + read func(p []byte) (n int, err error) + close func() error +} + +func (mock *mockReadCloser) Read(p []byte) (n int, err error) { + if mock.read != nil { + return mock.read(p) + } + return 0, nil +} + +func (mock *mockReadCloser) Close() error { + if mock.close != nil { + return mock.close() + } + return nil +} + +func TestPerformCheck_WithInvalidJSON(t *testing.T) { + key := getValidKey() + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader("Not a JSON document")), + }, nil + }, + } + + _, err := PerformCheck(client, createStandardProfile(), "clientSdkId", "https://apiUrl", key) + assert.Check(t, strings.Contains(err.Error(), "invalid character")) +} + +func TestPerformCheck_Success(t *testing.T) { + key := getValidKey() + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"on_fraud_list":true,"on_pep_list":true,"on_watch_list":true}`)), + }, nil + }, + } + + result, err := PerformCheck(client, createStandardProfile(), "clientSdkId", "https://apiUrl", key) + assert.NilError(t, err) + + assert.Check(t, result.OnFraudList) + assert.Check(t, result.OnPEPList) + assert.Check(t, result.OnWatchList) +} + +func TestPerformCheck_Unsuccessful(t *testing.T) { + key := getValidKey() + responseBody := "some service unavailable response" + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 503, + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + }, + } + + _, err := PerformCheck(client, createStandardProfile(), "clientSdkId", "https://apiUrl", key) + assert.ErrorContains(t, err, fmt.Sprintf("%d: AML Check was unsuccessful - %s", 503, responseBody)) + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, temporary && tempError.Temporary()) +} + +func TestPerformCheck_ShouldReturnMissingBaseURLError(t *testing.T) { + key := getValidKey() + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + }, nil + }, + } + + _, err := PerformCheck(client, createStandardProfile(), "clientSdkId", "", key) + assert.ErrorContains(t, err, "missing BaseURL") +} + +func TestPerformCheck_ShouldReturnBodyError(t *testing.T) { + key := getValidKey() + + body := &mockReadCloser{ + read: func(p []byte) (n int, err error) { + return 0, errors.New("some read error") + }, + } + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: body, + }, nil + }, + } + + _, err := PerformCheck(client, createStandardProfile(), "clientSdkId", "https://apiUrl", key) + assert.ErrorContains(t, err, "some read error") +} + +func getValidKey() *rsa.PrivateKey { + return test.GetValidKey("../test/test-key.pem") +} + +func createStandardProfile() Profile { + var amlAddress = Address{ + Country: "GBR"} + + var amlProfile = Profile{ + GivenNames: "Edward Richard George", + FamilyName: "Heath", + Address: amlAddress} + + return amlProfile +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/client.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/client.go new file mode 100644 index 0000000..d123d92 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/client.go @@ -0,0 +1,87 @@ +package yoti + +import ( + "crypto/rsa" + "os" + + "github.com/getyoti/yoti-go-sdk/v3/requests" + + "github.com/getyoti/yoti-go-sdk/v3/aml" + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/dynamic" + "github.com/getyoti/yoti-go-sdk/v3/profile" +) + +const apiDefaultURL = "https://api.yoti.com/api/v1" + +// Client represents a client that can communicate with yoti and return information about Yoti users. +type Client struct { + // SdkID represents the SDK ID and NOT the App ID. This can be found in the integration section of your + // application hub at https://hub.yoti.com/ + SdkID string + + // Key should be the security key given to you by yoti (see: security keys section of + // https://hub.yoti.com) for more information about how to load your key from a file see: + // https://github.com/getyoti/yoti-go-sdk/blob/master/README.md + Key *rsa.PrivateKey + + apiURL string + HTTPClient requests.HttpClient // Mockable HTTP Client Interface +} + +// NewClient constructs a Client object +func NewClient(sdkID string, key []byte) (*Client, error) { + decodedKey, err := cryptoutil.ParseRSAKey(key) + + if err != nil { + return nil, err + } + + return &Client{ + SdkID: sdkID, + Key: decodedKey, + }, err +} + +// OverrideAPIURL overrides the default API URL for this Yoti Client +func (client *Client) OverrideAPIURL(apiURL string) { + client.apiURL = apiURL +} + +func (client *Client) getAPIURL() string { + if client.apiURL != "" { + return client.apiURL + } + + if value, exists := os.LookupEnv("YOTI_API_URL"); exists && value != "" { + return value + } + + return apiDefaultURL +} + +// GetSdkID gets the Client SDK ID attached to this client instance +func (client *Client) GetSdkID() string { + return client.SdkID +} + +// GetActivityDetails requests information about a Yoti user using the one time +// use token generated by the Yoti login process. It returns the outcome of the +// request. If the request was successful it will include the user's details, +// otherwise an error will be returned, which will specify the reason the +// request failed. If the function call can be reattempted with the same token +// the error will implement interface{ Temporary() bool }. +func (client *Client) GetActivityDetails(token string) (activity profile.ActivityDetails, err error) { + return profile.GetActivityDetails(client.HTTPClient, token, client.GetSdkID(), client.getAPIURL(), client.Key) +} + +// PerformAmlCheck performs an Anti Money Laundering Check (AML) for a particular user. +// Returns three boolean values: 'OnPEPList', 'OnWatchList' and 'OnFraudList'. +func (client *Client) PerformAmlCheck(profile aml.Profile) (result aml.Result, err error) { + return aml.PerformCheck(client.HTTPClient, profile, client.GetSdkID(), client.getAPIURL(), client.Key) +} + +// CreateShareURL creates a QR code for a specified dynamic scenario +func (client *Client) CreateShareURL(scenario *dynamic.Scenario) (share dynamic.ShareURL, err error) { + return dynamic.CreateShareURL(client.HTTPClient, scenario, client.GetSdkID(), client.getAPIURL(), client.Key) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/client_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/client_test.go new file mode 100644 index 0000000..44ba57d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/client_test.go @@ -0,0 +1,205 @@ +package yoti + +import ( + "crypto/rsa" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/aml" + "github.com/getyoti/yoti-go-sdk/v3/dynamic" + "github.com/getyoti/yoti-go-sdk/v3/test" + "gotest.tools/v3/assert" +) + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func TestNewClient(t *testing.T) { + key, err := os.ReadFile("./test/test-key.pem") + assert.NilError(t, err) + + _, err = NewClient("some-sdk-id", key) + assert.NilError(t, err) +} + +func TestNewClient_KeyLoad_Failure(t *testing.T) { + key, err := os.ReadFile("test/test-key-invalid-format.pem") + assert.NilError(t, err) + + _, err = NewClient("", key) + + assert.ErrorContains(t, err, "invalid key: not PEM-encoded") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestYotiClient_PerformAmlCheck(t *testing.T) { + key, err := os.ReadFile("./test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("some-sdk-id", key) + assert.NilError(t, err) + + client.HTTPClient = &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"on_fraud_list":true}`)), + }, nil + }, + } + + var amlAddress = aml.Address{ + Country: "GBR"} + + var amlProfile = aml.Profile{ + GivenNames: "Edward Richard George", + FamilyName: "Heath", + Address: amlAddress} + + result, err := client.PerformAmlCheck(amlProfile) + assert.NilError(t, err) + + assert.Check(t, result.OnFraudList) +} + +func TestYotiClient_CreateShareURL(t *testing.T) { + key, err := os.ReadFile("./test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("some-sdk-id", key) + assert.NilError(t, err) + + client.HTTPClient = &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{"qrcode":"https://code.yoti.com/some-qr","ref_id":"0"}`)), + }, nil + }, + } + + policy, err := (&dynamic.PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build() + assert.NilError(t, err) + + scenario, err := (&dynamic.ScenarioBuilder{}).WithPolicy(policy).Build() + assert.NilError(t, err) + + result, err := client.CreateShareURL(&scenario) + assert.NilError(t, err) + assert.Equal(t, result.ShareURL, "https://code.yoti.com/some-qr") +} + +func TestYotiClient_HttpFailure_ReturnsFailure(t *testing.T) { + key := getValidKey() + + client := Client{ + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 500, + }, nil + }, + }, + Key: key, + } + + _, err := client.GetActivityDetails(test.EncryptedToken) + + assert.Check(t, err != nil) + assert.ErrorContains(t, err, "unknown HTTP error") + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, temporary) + assert.Check(t, tempError.Temporary()) +} + +func TestYotiClient_HttpFailure_ReturnsProfileNotFound(t *testing.T) { + key := getValidKey() + + client := Client{ + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 404, + }, nil + }, + }, + Key: key, + } + + _, err := client.GetActivityDetails(test.EncryptedToken) + + assert.ErrorContains(t, err, "Profile not found") + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestClient_OverrideAPIURL_ShouldSetAPIURL(t *testing.T) { + client := &Client{} + expectedURL := "expectedurl.com" + client.OverrideAPIURL(expectedURL) + assert.Equal(t, client.getAPIURL(), expectedURL) +} + +func TestYotiClient_GetAPIURLUsesOverriddenBaseUrlOverEnvVariable(t *testing.T) { + client := Client{} + client.OverrideAPIURL("overridenBaseUrl") + + os.Setenv("YOTI_API_URL", "envBaseUrl") + + result := client.getAPIURL() + + assert.Equal(t, "overridenBaseUrl", result) +} + +func TestYotiClient_GetAPIURLUsesEnvVariable(t *testing.T) { + client := Client{} + + os.Setenv("YOTI_API_URL", "envBaseUrl") + + result := client.getAPIURL() + + assert.Equal(t, "envBaseUrl", result) +} + +func TestYotiClient_GetAPIURLUsesDefaultUrlAsFallbackWithEmptyEnvValue(t *testing.T) { + client := Client{} + + os.Setenv("YOTI_API_URL", "") + + result := client.getAPIURL() + + assert.Equal(t, "https://api.yoti.com/api/v1", result) +} + +func TestYotiClient_GetAPIURLUsesDefaultUrlAsFallbackWithNoEnvValue(t *testing.T) { + client := Client{} + + os.Unsetenv("YOTI_API_URL") + + result := client.getAPIURL() + + assert.Equal(t, "https://api.yoti.com/api/v1", result) +} + +func getValidKey() *rsa.PrivateKey { + return test.GetValidKey("test/test-key.pem") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/consts/attribute_names.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/consts/attribute_names.go new file mode 100644 index 0000000..21e3b23 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/consts/attribute_names.go @@ -0,0 +1,21 @@ +package consts + +// Attribute names for user profile attributes +const ( + AttrSelfie = "selfie" + AttrGivenNames = "given_names" + AttrFamilyName = "family_name" + AttrFullName = "full_name" + AttrMobileNumber = "phone_number" + AttrEmailAddress = "email_address" + AttrDateOfBirth = "date_of_birth" + AttrAddress = "postal_address" + AttrStructuredPostalAddress = "structured_postal_address" + AttrGender = "gender" + AttrNationality = "nationality" + AttrDocumentImages = "document_images" + AttrDocumentDetails = "document_details" + AttrIdentityProfileReport = "identity_profile_report" + AttrAgeOver = "age_over:%d" + AttrAgeUnder = "age_under:%d" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/consts/version.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/consts/version.go new file mode 100644 index 0000000..3c6de89 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/consts/version.go @@ -0,0 +1,6 @@ +package consts + +const ( + SDKIdentifier = "Go" + SDKVersionIdentifier = "3.14.0" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/cryptoutil/crypto_utils.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/cryptoutil/crypto_utils.go new file mode 100644 index 0000000..32b4e17 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/cryptoutil/crypto_utils.go @@ -0,0 +1,165 @@ +package cryptoutil + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/util" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" + "google.golang.org/protobuf/proto" +) + +// ParseRSAKey parses a PKCS1 private key from bytes +func ParseRSAKey(keyBytes []byte) (*rsa.PrivateKey, error) { + // Extract the PEM-encoded data + block, _ := pem.Decode(keyBytes) + + if block == nil { + return nil, errors.New("invalid key: not PEM-encoded") + } + + if block.Type != "RSA PRIVATE KEY" { + return nil, errors.New("invalid key: not RSA private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, errors.New("invalid key: bad RSA private key") + } + + return key, nil +} + +// nolint: gosec +func decryptRsa(cipherBytes []byte, key *rsa.PrivateKey) ([]byte, error) { + return rsa.DecryptPKCS1v15(rand.Reader, key, cipherBytes) +} + +// DecipherAes deciphers AES-encrypted bytes +func DecipherAes(key, iv, cipherBytes []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return []byte{}, err + } + + // CBC mode always works in whole blocks. + if (len(cipherBytes) % aes.BlockSize) != 0 { + return []byte{}, errors.New("ciphertext is not a multiple of the block size") + } + + mode := cipher.NewCBCDecrypter(block, iv) + + decipheredBytes := make([]byte, len(cipherBytes)) + + mode.CryptBlocks(decipheredBytes, cipherBytes) + + return pkcs7Unpad(decipheredBytes, aes.BlockSize) +} + +func pkcs7Unpad(ciphertext []byte, blocksize int) (result []byte, err error) { + if blocksize <= 0 { + err = fmt.Errorf("blocksize %d is not valid for padding removal", blocksize) + return + } + if len(ciphertext) == 0 { + err = errors.New("cannot remove padding on empty byte array") + return + } + if len(ciphertext)%blocksize != 0 { + err = errors.New("ciphertext is not a multiple of the block size") + return + } + + c := ciphertext[len(ciphertext)-1] + n := int(c) + if n == 0 || n > len(ciphertext) { + err = errors.New("ciphertext is not padded with PKCS#7 padding") + return + } + + // verify all padding bytes are correct + for i := 0; i < n; i++ { + if ciphertext[len(ciphertext)-n+i] != c { + err = errors.New("ciphertext is not padded with PKCS#7 padding") + return + } + } + return ciphertext[:len(ciphertext)-n], nil +} + +// DecryptToken decrypts an RSA-encrypted token, using the specified RSA private key +func DecryptToken(encryptedConnectToken string, key *rsa.PrivateKey) (result string, err error) { + // token was encoded as a urlsafe base64 so it can be transferred in a url + var cipherBytes []byte + if cipherBytes, err = util.UrlSafeBase64ToBytes(encryptedConnectToken); err != nil { + return "", err + } + + var decipheredBytes []byte + if decipheredBytes, err = decryptRsa(cipherBytes, key); err != nil { + return "", err + } + + return string(decipheredBytes), nil +} + +// UnwrapKey unwraps an RSA private key +func UnwrapKey(wrappedKey string, key *rsa.PrivateKey) (result []byte, err error) { + var cipherBytes []byte + if cipherBytes, err = util.Base64ToBytes(wrappedKey); err != nil { + return nil, err + } + return decryptRsa(cipherBytes, key) +} + +func decryptAESGCM(cipherText, iv, secret []byte) ([]byte, error) { + block, err := aes.NewCipher(secret) + if err != nil { + return nil, fmt.Errorf("failed to create new aes cipher: %v", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create new gcm cipher: %v", err) + } + + plainText, err := gcm.Open(nil, iv, cipherText, nil) + if err != nil { + return nil, fmt.Errorf("failed to decrypt receipt key: %v", err) + } + + return plainText, nil +} + +func UnwrapReceiptKey(wrappedReceiptKey []byte, encryptedItemKey []byte, itemKeyIv []byte, key *rsa.PrivateKey) ([]byte, error) { + decryptedItemKey, err := decryptRsa(encryptedItemKey, key) + if err != nil { + return nil, fmt.Errorf("failed to decrypt item key: %v", err) + } + + plainText, err := decryptAESGCM(wrappedReceiptKey, itemKeyIv, decryptedItemKey) + if err != nil { + return nil, fmt.Errorf("failed to decrypt receipt key: %v", err) + } + return plainText, nil +} + +func DecryptReceiptContent(content, receiptContentKey []byte) ([]byte, error) { + if content == nil { + return nil, fmt.Errorf("failed to decrypt receipt content is nil") + } + + decodedData := &yotiprotocom.EncryptedData{} + err := proto.Unmarshal(content, decodedData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshall content: %v", content) + } + + return DecipherAes(receiptContentKey, decodedData.Iv, decodedData.CipherText) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/cryptoutil/crypto_utils_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/cryptoutil/crypto_utils_test.go new file mode 100644 index 0000000..864e2f3 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/cryptoutil/crypto_utils_test.go @@ -0,0 +1,176 @@ +package cryptoutil + +import ( + "crypto/aes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/file" + "github.com/getyoti/yoti-go-sdk/v3/util" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" +) + +const ( + wrappedKey = "kyHPjq2+Y48cx+9yS/XzmW09jVUylSdhbP+3Q9Tc9p6bCEnyfa8vj38AIu744RzzE+Dc4qkSF21VfzQKtJVILfOXu5xRc7MYa5k3zWhjiesg/gsrv7J4wDyyBpHIJB8TWXnubYMbSYQJjlsfwyxE9kGe0YI08pRo2Tiht0bfR5Z/YrhAk4UBvjp84D+oyug/1mtGhKphA4vgPhQ9/y2wcInYxju7Q6yzOsXGaRUXR38Tn2YmY9OBgjxiTnhoYJFP1X9YJkHeWMW0vxF1RHxgIVrpf7oRzdY1nq28qzRg5+wC7cjRpS2i/CKUAo0oVG4pbpXsaFhaTewStVC7UFtA77JHb3EnF4HcSWMnK5FM7GGkL9MMXQenh11NZHKPWXpux0nLZ6/vwffXZfsiyTIcFL/NajGN8C/hnNBljoQ+B3fzWbjcq5ueUOPwARZ1y38W83UwMynzkud/iEdHLaZIu4qUCRkfSxJg7Dc+O9/BdiffkOn2GyFmNjVeq754DCUypxzMkjYxokedN84nK13OU4afVyC7t5DDxAK/MqAc69NCBRLqMi5f8BMeOZfMcSWPGC9a2Qu8VgG125TuZT4+wIykUhGyj3Bb2/fdPsxwuKFR+E0uqs0ZKvcv1tkNRRtKYBqTacgGK9Yoehg12cyLrITLdjU1fmIDn4/vrhztN5w=" + b64EncryptedData = "ChCZAib1TBm9Q5GYfFrS1ep9EnAwQB5shpAPWLBgZgFgt6bCG3S5qmZHhrqUbQr3yL6yeLIDwbM7x4nuT/MYp+LDXgmFTLQNYbDTzrEzqNuO2ZPn9Kpg+xpbm9XtP7ZLw3Ep2BCmSqtnll/OdxAqLb4DTN4/wWdrjnFC+L/oQEECu646" + encryptedToken = "b6H19bUCJhwh6WqQX_sEHWX9RP-A_ANr1fkApwA4Dp2nJQFAjrF9e6YCXhNBpAIhfHnN0iXubyXxXZMNwNMSQ5VOxkqiytrvPykfKQWHC6ypSbfy0ex8ihndaAXG5FUF-qcU8QaFPMy6iF3x0cxnY0Ij0kZj0Ng2t6oiNafb7AhT-VGXxbFbtZu1QF744PpWMuH0LVyBsAa5N5GJw2AyBrnOh67fWMFDKTJRziP5qCW2k4h5vJfiYr_EOiWKCB1d_zINmUm94ZffGXxcDAkq-KxhN1ZuNhGlJ2fKcFh7KxV0BqlUWPsIEiwS0r9CJ2o1VLbEs2U_hCEXaqseEV7L29EnNIinEPVbL4WR7vkF6zQCbK_cehlk2Qwda-VIATqupRO5grKZN78R9lBitvgilDaoE7JB_VFcPoljGQ48kX0wje1mviX4oJHhuO8GdFITS5LTbojGVQWT7LUNgAUe0W0j-FLHYYck3v84OhWTqads5_jmnnLkp9bdJSRuJF0e8pNdePnn2lgF-GIcyW_0kyGVqeXZrIoxnObLpF-YeUteRBKTkSGFcy7a_V_DLiJMPmH8UXDLOyv8TVt3ppzqpyUrLN2JVMbL5wZ4oriL2INEQKvw_boDJjZDGeRlu5m1y7vGDNBRDo64-uQM9fRUULPw-YkABNwC0DeShswzT00=" +) + +func TestCryptoUtil_ParseRSAKey(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + block := pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + keyFileName := "tmpKey.pem" + keyFile, err := os.Create(keyFileName) + assert.NilError(t, err) + + err = pem.Encode(keyFile, &block) + assert.NilError(t, err) + + err = keyFile.Close() + assert.NilError(t, err) + + var keyBytes []byte + keyBytes, err = file.ReadFile(keyFileName) + assert.NilError(t, err) + + key, err = ParseRSAKey(keyBytes) + assert.NilError(t, err) + assert.Check(t, key != nil) +} + +func TestCryptoutil_ParseRSAKey_PublicKeyShouldFail(t *testing.T) { + testPEM := []byte(`-----BEGIN RSA PUBLIC KEY----- +VGVzdCBTdHJpbmc= +-----END RSA PUBLIC KEY-----`) + + _, err := ParseRSAKey(testPEM) + + assert.Error(t, err, "invalid key: not RSA private key") +} + +func TestCryptoutil_ParseRSAKey_InvalidKeyShouldFail(t *testing.T) { + testPEM := []byte(`-----BEGIN RSA PRIVATE KEY----- +VGVzdCBTdHJpbmc= +-----END RSA PRIVATE KEY-----`) + + _, err := ParseRSAKey(testPEM) + + assert.Error(t, err, "invalid key: bad RSA private key") +} + +func TestCryptoutil_ParseRSAKey_InvalidShouldFail(t *testing.T) { + var testPEM []byte + + _, err := ParseRSAKey(testPEM) + + assert.Error(t, err, "invalid key: not PEM-encoded") +} + +func TestCryptoutil_DecipherAes(t *testing.T) { + unwrappedKey, err := UnwrapKey(wrappedKey, getKey()) + if err != nil { + return + } + + encryptedBytes, err := util.Base64ToBytes(b64EncryptedData) + if err != nil || len(encryptedBytes) == 0 { + assert.NilError(t, err) + } + encryptedData := &yotiprotocom.EncryptedData{} + err = proto.Unmarshal(encryptedBytes, encryptedData) + assert.NilError(t, err) + + bytes, err := DecipherAes(unwrappedKey, encryptedData.Iv, encryptedData.CipherText) + assert.NilError(t, err) + assert.Check(t, bytes != nil) +} + +func TestCryptoutil_DecipherAes_EmptyCiphertextShouldError(t *testing.T) { + _, err := DecipherAes([]byte{}, []byte{}, []byte{}) + assert.Check(t, err != nil) +} + +func TestCryptoutil_DecipherAes_CiphertextNotMatchingBlocksizeShouldError(t *testing.T) { + _, err := DecipherAes( + make([]byte, 16), + []byte{}, + make([]byte, aes.BlockSize+1), + ) + assert.Check(t, err != nil) +} + +func TestCryptoutil_pkcs7Unpad_InvalidBlocksizeShouldError(t *testing.T) { + _, err := pkcs7Unpad([]byte{}, -1) + assert.Error(t, err, "blocksize -1 is not valid for padding removal") +} + +func TestCryptoutil_pkcs7Unpad_EmptyByteArrayShouldError(t *testing.T) { + _, err := pkcs7Unpad([]byte{}, 1) + assert.Error(t, err, "cannot remove padding on empty byte array") +} + +func TestCryptoutil_pkcs7Unpad_CiphertextNotMultipleOfBlocksizeShouldError(t *testing.T) { + _, err := pkcs7Unpad([]byte{0, 0, 0}, 2) + assert.Error(t, err, "ciphertext is not a multiple of the block size") +} + +func TestCryptoutil_pkcs7Unpad_CiphertextNotPaddedShouldError(t *testing.T) { + _, err := pkcs7Unpad([]byte{0xF, 0x0, 0x0}, 3) + assert.Error(t, err, "ciphertext is not padded with PKCS#7 padding") +} + +func TestCryptoutil_pkcs7Unpad_CiphertextPaddingIncorrect(t *testing.T) { + _, err := pkcs7Unpad([]byte{0x1, 0x1, 0x1, 0xF, 0x0, 0x0, 0xB, 0xA, 0x7}, 3) + assert.Error(t, err, "ciphertext is not padded with PKCS#7 padding") +} + +func TestCryptoutil_DecryptToken(t *testing.T) { + _, err := DecryptToken(encryptedToken, getKey()) + assert.NilError(t, err) +} + +func TestCryptoutil_DecryptToken_InvalidToken(t *testing.T) { + _, err := DecryptToken("c29tZS10b2tlbg==", getKey()) + assert.Error(t, err, "crypto/rsa: decryption error") +} + +func TestCryptoutil_DecryptToken_InvalidBase64ShouldError(t *testing.T) { + _, err := DecryptToken("Not a Token", nil) + assert.Check(t, err != nil) +} + +func TestCryptoutil_UnwrapKey(t *testing.T) { + unwrappedKey, err := UnwrapKey(wrappedKey, getKey()) + + assert.NilError(t, err) + assert.Check(t, unwrappedKey != nil) +} + +func TestCryptoutil_UnwrapKey_InvalidBase64ShouldError(t *testing.T) { + _, err := UnwrapKey("Not b64 encoded", nil) + assert.Check(t, err != nil) +} + +func getKey() (key *rsa.PrivateKey) { + keyBytes, err := os.ReadFile("../test/test-key.pem") + if err != nil { + panic("Error reading the test key: " + err.Error()) + } + + key, err = ParseRSAKey(keyBytes) + if err != nil { + panic("Error parsing the test key: " + err.Error()) + } + + return key +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digital_identity_client.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digital_identity_client.go new file mode 100644 index 0000000..b074712 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digital_identity_client.go @@ -0,0 +1,88 @@ +package yoti + +import ( + "crypto/rsa" + "os" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/digitalidentity" + "github.com/getyoti/yoti-go-sdk/v3/requests" +) + +const DefaultURL = "https://api.yoti.com/share" + +// DigitalIdentityClient represents a client that can communicate with yoti and return information about Yoti users. +type DigitalIdentityClient struct { + // SdkID represents the SDK ID and NOT the App ID. This can be found in the integration section of your + // application hub at https://hub.yoti.com/ + SdkID string + + // Key should be the security key given to you by yoti (see: security keys section of + // https://hub.yoti.com) for more information about how to load your key from a file see: + // https://github.com/getyoti/yoti-go-sdk/blob/master/README.md + Key *rsa.PrivateKey + + apiURL string + HTTPClient requests.HttpClient // Mockable HTTP Client Interface +} + +// NewDigitalIdentityClient constructs a Client object +func NewDigitalIdentityClient(sdkID string, key []byte) (*DigitalIdentityClient, error) { + decodedKey, err := cryptoutil.ParseRSAKey(key) + + if err != nil { + return nil, err + } + + return &DigitalIdentityClient{ + SdkID: sdkID, + Key: decodedKey, + }, err +} + +// OverrideAPIURL overrides the default API URL for this Yoti Client +func (client *DigitalIdentityClient) OverrideAPIURL(apiURL string) { + client.apiURL = apiURL +} + +func (client *DigitalIdentityClient) getAPIURL() string { + if client.apiURL != "" { + return client.apiURL + } + + if value, exists := os.LookupEnv("YOTI_API_URL"); exists && value != "" { + return value + } + + return DefaultURL +} + +// GetSdkID gets the Client SDK ID attached to this client instance +func (client *DigitalIdentityClient) GetSdkID() string { + return client.SdkID +} + +// CreateShareSession creates a sharing session to initiate a sharing process based on a policy +func (client *DigitalIdentityClient) CreateShareSession(shareSessionRequest *digitalidentity.ShareSessionRequest) (shareSession *digitalidentity.ShareSession, err error) { + return digitalidentity.CreateShareSession(client.HTTPClient, shareSessionRequest, client.GetSdkID(), client.getAPIURL(), client.Key) +} + +// GetShareSession retrieves the sharing session. +func (client *DigitalIdentityClient) GetShareSession(sessionID string) (*digitalidentity.ShareSession, error) { + return digitalidentity.GetShareSession(client.HTTPClient, sessionID, client.GetSdkID(), client.getAPIURL(), client.Key) +} + +// CreateShareQrCode generates a sharing session QR code to initiate a sharing process based on session ID +func (client *DigitalIdentityClient) CreateShareQrCode(sessionID string) (share *digitalidentity.QrCode, err error) { + return digitalidentity.CreateShareQrCode(client.HTTPClient, sessionID, client.GetSdkID(), client.getAPIURL(), client.Key) +} + +// Get session QR code based on generated Qr ID +func (client *DigitalIdentityClient) GetQrCode(qrCodeId string) (share digitalidentity.ShareSessionQrCode, err error) { + return digitalidentity.GetShareSessionQrCode(client.HTTPClient, qrCodeId, client.GetSdkID(), client.getAPIURL(), client.Key) +} + +// GetShareReceipt fetches the receipt of the share given a receipt id. +func (client *DigitalIdentityClient) GetShareReceipt(receiptId string) (share digitalidentity.SharedReceiptResponse, err error) { + return digitalidentity.GetShareReceipt(client.HTTPClient, receiptId, client.GetSdkID(), client.getAPIURL(), client.Key) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digital_identity_client_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digital_identity_client_test.go new file mode 100644 index 0000000..3c85d93 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digital_identity_client_test.go @@ -0,0 +1,168 @@ +package yoti + +import ( + "crypto/rsa" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/digitalidentity" + "github.com/getyoti/yoti-go-sdk/v3/test" + "gotest.tools/v3/assert" +) + +func TestDigitalIDClient(t *testing.T) { + key, err := os.ReadFile("./test/test-key.pem") + assert.NilError(t, err) + + _, err = NewDigitalIdentityClient("some-sdk-id", key) + assert.NilError(t, err) +} + +func TestDigitalIDClient_KeyLoad_Failure(t *testing.T) { + key, err := os.ReadFile("test/test-key-invalid-format.pem") + assert.NilError(t, err) + + _, err = NewDigitalIdentityClient("", key) + + assert.ErrorContains(t, err, "invalid key: not PEM-encoded") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestYotiClient_CreateShareSession(t *testing.T) { + key, err := os.ReadFile("./test/test-key.pem") + assert.NilError(t, err) + + client, err := NewDigitalIdentityClient("some-sdk-id", key) + assert.NilError(t, err) + + client.HTTPClient = &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{"id":"SOME_ID","status":"SOME_STATUS","expiry":"SOME_EXPIRY","created":"SOME_CREATED","updated":"SOME_UPDATED","qrCode":{"id":"SOME_QRCODE_ID"},"receipt":{"id":"SOME_RECEIPT_ID"}}`)), + }, nil + }, + } + + policy, err := (&digitalidentity.PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build() + assert.NilError(t, err) + + session, err := (&digitalidentity.ShareSessionRequestBuilder{}).WithPolicy(policy).Build() + assert.NilError(t, err) + + result, err := client.CreateShareSession(&session) + + assert.NilError(t, err) + assert.Equal(t, result.Status, "SOME_STATUS") +} + +func TestDigitalIDClient_HttpFailure_ReturnsUnKnownHttpError(t *testing.T) { + key := getDigitalValidKey() + client := DigitalIdentityClient{ + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 401, + }, nil + }, + }, + Key: key, + } + + _, err := client.GetShareSession("SOME ID") + + assert.ErrorContains(t, err, "unknown HTTP error") + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestDigitalIDClient_GetSession(t *testing.T) { + key, err := os.ReadFile("./test/test-key.pem") + if err != nil { + t.Fatalf("failed to read pem file :: %v", err) + } + + mockSessionID := "SOME_SESSION_ID" + client, err := NewDigitalIdentityClient("some-sdk-id", key) + if err != nil { + t.Fatalf("failed to build the DigitalIdClient :: %v", err) + } + + client.HTTPClient = &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"id":"SOME_ID","status":"SOME_STATUS","expiry":"SOME_EXPIRY","created":"SOME_CREATED","updated":"SOME_UPDATED","qrCode":{"id":"SOME_QRCODE_ID"},"receipt":{"id":"SOME_RECEIPT_ID"}}`)), + }, nil + }, + } + + result, err := client.GetShareSession(mockSessionID) + if err != nil { + t.Fatalf("failed to GetShareSesssion :: %v", err) + } + + assert.Equal(t, result.Id, "SOME_ID") + assert.Equal(t, result.Status, "SOME_STATUS") + assert.Equal(t, result.Created, "SOME_CREATED") + +} + +func TestDigitalIDClient_OverrideAPIURL_ShouldSetAPIURL(t *testing.T) { + client := &DigitalIdentityClient{} + + expectedURL := "expectedurl.com" + client.OverrideAPIURL(expectedURL) + + assert.Equal(t, client.getAPIURL(), expectedURL) +} + +func TestDigitalIDClient_GetAPIURLUsesOverriddenBaseUrlOverEnvVariable(t *testing.T) { + client := DigitalIdentityClient{} + client.OverrideAPIURL("overridenBaseUrl") + + os.Setenv("YOTI_API_URL", "envBaseUrl") + result := client.getAPIURL() + + assert.Equal(t, "overridenBaseUrl", result) +} + +func TestDigitalIDClient_GetAPIURLUsesEnvVariable(t *testing.T) { + client := DigitalIdentityClient{} + + os.Setenv("YOTI_API_URL", "envBaseUrl") + result := client.getAPIURL() + + assert.Equal(t, "envBaseUrl", result) +} + +func TestDigitalIDClient_GetAPIURLUsesDefaultUrlAsFallbackWithEmptyEnvValue(t *testing.T) { + client := DigitalIdentityClient{} + + os.Setenv("YOTI_API_URL", "") + result := client.getAPIURL() + + assert.Equal(t, "https://api.yoti.com/share", result) +} + +func TestDigitalIDClient_GetAPIURLUsesDefaultUrlAsFallbackWithNoEnvValue(t *testing.T) { + client := DigitalIdentityClient{} + + os.Unsetenv("YOTI_API_URL") + result := client.getAPIURL() + + assert.Equal(t, "https://api.yoti.com/share", result) +} + +func getDigitalValidKey() *rsa.PrivateKey { + return test.GetValidKey("test/test-key.pem") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/address.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/address.go new file mode 100644 index 0000000..17bfb51 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/address.go @@ -0,0 +1,52 @@ +package digitalidentity + +import ( + "reflect" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +func getFormattedAddress(profile *UserProfile, formattedAddress string) *yotiprotoattr.Attribute { + proto := getProtobufAttribute(*profile, consts.AttrStructuredPostalAddress) + + return &yotiprotoattr.Attribute{ + Name: consts.AttrAddress, + Value: []byte(formattedAddress), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: proto.Anchors, + } +} + +func ensureAddressProfile(p *UserProfile) *attribute.StringAttribute { + if structuredPostalAddress, err := p.StructuredPostalAddress(); err == nil { + if (structuredPostalAddress != nil && !reflect.DeepEqual(structuredPostalAddress, attribute.JSONAttribute{})) { + var formattedAddress string + formattedAddress, err = retrieveFormattedAddressFromStructuredPostalAddress(structuredPostalAddress.Value()) + if err == nil && formattedAddress != "" { + return attribute.NewString(getFormattedAddress(p, formattedAddress)) + } + } + } + + return nil +} + +func retrieveFormattedAddressFromStructuredPostalAddress(structuredPostalAddress interface{}) (address string, err error) { + parsedStructuredAddressMap := structuredPostalAddress.(map[string]interface{}) + if formattedAddress, ok := parsedStructuredAddressMap["formatted_address"]; ok { + return formattedAddress.(string), nil + } + return +} + +func getProtobufAttribute(profile UserProfile, key string) *yotiprotoattr.Attribute { + for _, v := range profile.attributeSlice { + if v.Name == key { + return v + } + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/application_profile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/application_profile.go new file mode 100644 index 0000000..8fae7bd --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/application_profile.go @@ -0,0 +1,50 @@ +package digitalidentity + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// Attribute names for application attributes +const ( + AttrConstApplicationName = "application_name" + AttrConstApplicationURL = "application_url" + AttrConstApplicationLogo = "application_logo" + AttrConstApplicationReceiptBGColor = "application_receipt_bgcolor" +) + +// ApplicationProfile is the profile of an application with convenience methods +// to access well-known attributes. +type ApplicationProfile struct { + baseProfile +} + +func newApplicationProfile(attributes *yotiprotoattr.AttributeList) ApplicationProfile { + return ApplicationProfile{ + baseProfile{ + attributeSlice: createAttributeSlice(attributes), + }, + } +} + +// ApplicationName is the name of the application +func (p ApplicationProfile) ApplicationName() *attribute.StringAttribute { + return p.GetStringAttribute(AttrConstApplicationName) +} + +// ApplicationURL is the URL where the application is available at +func (p ApplicationProfile) ApplicationURL() *attribute.StringAttribute { + return p.GetStringAttribute(AttrConstApplicationURL) +} + +// ApplicationReceiptBgColor is the background colour that will be displayed on +// each receipt the user gets as a result of a share with the application. +func (p ApplicationProfile) ApplicationReceiptBgColor() *attribute.StringAttribute { + return p.GetStringAttribute(AttrConstApplicationReceiptBGColor) +} + +// ApplicationLogo is the logo of the application that will be displayed to +// those users that perform a share with it. +func (p ApplicationProfile) ApplicationLogo() *attribute.ImageAttribute { + return p.GetImageAttribute(AttrConstApplicationLogo) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/age_verifications.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/age_verifications.go new file mode 100644 index 0000000..a7655d0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/age_verifications.go @@ -0,0 +1,34 @@ +package attribute + +import ( + "strconv" + "strings" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// AgeVerification encapsulates the result of a single age verification +// as part of a share +type AgeVerification struct { + Age int + CheckType string + Result bool + Attribute *yotiprotoattr.Attribute +} + +// NewAgeVerification constructs an AgeVerification from a protobuffer +func NewAgeVerification(attr *yotiprotoattr.Attribute) (verification AgeVerification, err error) { + split := strings.Split(attr.Name, ":") + verification.Age, err = strconv.Atoi(split[1]) + verification.CheckType = split[0] + + if string(attr.Value) == "true" { + verification.Result = true + } else { + verification.Result = false + } + + verification.Attribute = attr + + return +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/age_verifications_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/age_verifications_test.go new file mode 100644 index 0000000..b3a6e08 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/age_verifications_test.go @@ -0,0 +1,42 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestNewAgeVerification_ValueTrue(t *testing.T) { + attribute := &yotiprotoattr.Attribute{ + Name: "age_over:18", + Value: []byte("true"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + ageVerification, err := NewAgeVerification(attribute) + + assert.NilError(t, err) + + assert.Equal(t, ageVerification.Age, 18) + assert.Equal(t, ageVerification.CheckType, "age_over") + assert.Equal(t, ageVerification.Result, true) +} + +func TestNewAgeVerification_ValueFalse(t *testing.T) { + attribute := &yotiprotoattr.Attribute{ + Name: "age_under:30", + Value: []byte("false"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + ageVerification, err := NewAgeVerification(attribute) + + assert.NilError(t, err) + + assert.Equal(t, ageVerification.Age, 30) + assert.Equal(t, ageVerification.CheckType, "age_under") + assert.Equal(t, ageVerification.Result, false) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchor_parser.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchor_parser.go new file mode 100644 index 0000000..d1476c4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchor_parser.go @@ -0,0 +1,110 @@ +package anchor + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" + "google.golang.org/protobuf/proto" +) + +type anchorExtension struct { + Extension string `asn1:"tag:0,utf8"` +} + +var ( + sourceOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 47127, 1, 1, 1} + verifierOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 47127, 1, 1, 2} +) + +// ParseAnchors takes a slice of protobuf anchors, parses them, and returns a slice of Yoti SDK Anchors +func ParseAnchors(protoAnchors []*yotiprotoattr.Anchor) []*Anchor { + var processedAnchors []*Anchor + for _, protoAnchor := range protoAnchors { + parsedCerts := parseCertificates(protoAnchor.OriginServerCerts) + + anchorType, extension := getAnchorValuesFromCertificate(parsedCerts) + + parsedSignedTimestamp, err := parseSignedTimestamp(protoAnchor.SignedTimeStamp) + if err != nil { + continue + } + + processedAnchor := newAnchor(anchorType, parsedCerts, parsedSignedTimestamp, protoAnchor.SubType, extension) + + processedAnchors = append(processedAnchors, processedAnchor) + } + + return processedAnchors +} + +func getAnchorValuesFromCertificate(parsedCerts []*x509.Certificate) (anchorType Type, extension string) { + defaultAnchorType := TypeUnknown + + for _, cert := range parsedCerts { + for _, ext := range cert.Extensions { + var ( + value string + err error + ) + parsedAnchorType, value, err := parseExtension(ext) + if err != nil { + continue + } else if parsedAnchorType == TypeUnknown { + continue + } + return parsedAnchorType, value + } + } + + return defaultAnchorType, "" +} + +func parseExtension(ext pkix.Extension) (anchorType Type, val string, err error) { + anchorType = TypeUnknown + + switch { + case ext.Id.Equal(sourceOID): + anchorType = TypeSource + case ext.Id.Equal(verifierOID): + anchorType = TypeVerifier + default: + return anchorType, "", nil + } + + var ae anchorExtension + _, err = asn1.Unmarshal(ext.Value, &ae) + switch { + case err != nil: + return anchorType, "", fmt.Errorf("unable to unmarshal extension: %v", err) + case len(ae.Extension) == 0: + return anchorType, "", errors.New("empty extension") + default: + val = ae.Extension + } + + return anchorType, val, nil +} + +func parseSignedTimestamp(rawBytes []byte) (*yotiprotocom.SignedTimestamp, error) { + signedTimestamp := &yotiprotocom.SignedTimestamp{} + if err := proto.Unmarshal(rawBytes, signedTimestamp); err != nil { + return signedTimestamp, err + } + + return signedTimestamp, nil +} + +func parseCertificates(rawCerts [][]byte) (result []*x509.Certificate) { + for _, cert := range rawCerts { + parsedCertificate, _ := x509.ParseCertificate(cert) + + result = append(result, parsedCertificate) + } + + return result +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchor_parser_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchor_parser_test.go new file mode 100644 index 0000000..13849a3 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchor_parser_test.go @@ -0,0 +1,147 @@ +package anchor + +import ( + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" +) + +func assertServerCertSerialNo(t *testing.T, expectedSerialNo string, actualSerialNo *big.Int) { + expectedSerialNoBigInt := new(big.Int) + expectedSerialNoBigInt, ok := expectedSerialNoBigInt.SetString(expectedSerialNo, 10) + assert.Assert(t, ok, "Unexpected error when setting string as big int") + + assert.Equal(t, expectedSerialNoBigInt.Cmp(actualSerialNo), 0) // 0 == equivalent +} + +func createAnchorSliceFromTestFile(t *testing.T, filename string) []*yotiprotoattr.Anchor { + anchorBytes := test.DecodeTestFile(t, filename) + + protoAnchor := &yotiprotoattr.Anchor{} + err2 := proto.Unmarshal(anchorBytes, protoAnchor) + assert.NilError(t, err2) + + protoAnchors := append([]*yotiprotoattr.Anchor{}, protoAnchor) + + return protoAnchors +} + +func TestAnchorParser_parseExtension_ShouldErrorForInvalidExtension(t *testing.T) { + invalidExt := pkix.Extension{ + Id: sourceOID, + } + + _, _, err := parseExtension(invalidExt) + + assert.Check(t, err != nil) + assert.Error(t, err, "unable to unmarshal extension: asn1: syntax error: sequence truncated") +} + +func TestAnchorParser_Passport(t *testing.T) { + anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_passport.txt") + + parsedAnchors := ParseAnchors(anchorSlice) + + actualAnchor := parsedAnchors[0] + + assert.Equal(t, actualAnchor.Type(), TypeSource) + + expectedDate := time.Date(2018, time.April, 12, 13, 14, 32, 835537e3, time.UTC) + actualDate := actualAnchor.SignedTimestamp().Timestamp().UTC() + assert.Equal(t, actualDate, expectedDate) + + expectedSubType := "OCR" + assert.Equal(t, actualAnchor.SubType(), expectedSubType) + + expectedValue := "PASSPORT" + assert.Equal(t, actualAnchor.Value(), expectedValue) + + actualSerialNo := actualAnchor.OriginServerCerts()[0].SerialNumber + assertServerCertSerialNo(t, "277870515583559162487099305254898397834", actualSerialNo) +} + +func TestAnchorParser_DrivingLicense(t *testing.T) { + anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_driving_license.txt") + + parsedAnchors := ParseAnchors(anchorSlice) + resultAnchor := parsedAnchors[0] + + assert.Equal(t, resultAnchor.Type(), TypeSource) + + expectedDate := time.Date(2018, time.April, 11, 12, 13, 3, 923537e3, time.UTC) + actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC() + assert.Equal(t, actualDate, expectedDate) + + expectedSubType := "" + assert.Equal(t, resultAnchor.SubType(), expectedSubType) + + expectedValue := "DRIVING_LICENCE" + assert.Equal(t, resultAnchor.Value(), expectedValue) + + actualSerialNo := resultAnchor.OriginServerCerts()[0].SerialNumber + assertServerCertSerialNo(t, "46131813624213904216516051554755262812", actualSerialNo) +} + +func TestAnchorParser_UnknownAnchor(t *testing.T) { + anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_unknown.txt") + + resultAnchor := ParseAnchors(anchorSlice)[0] + + expectedDate := time.Date(2019, time.March, 5, 10, 45, 11, 840037e3, time.UTC) + actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC() + assert.Equal(t, actualDate, expectedDate) + + expectedSubType := "TEST UNKNOWN SUB TYPE" + expectedType := TypeUnknown + assert.Equal(t, resultAnchor.SubType(), expectedSubType) + assert.Equal(t, resultAnchor.Type(), expectedType) + assert.Equal(t, resultAnchor.Value(), "") +} + +func TestAnchorParser_YotiAdmin(t *testing.T) { + anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_yoti_admin.txt") + + resultAnchor := ParseAnchors(anchorSlice)[0] + + assert.Equal(t, resultAnchor.Type(), TypeVerifier) + + expectedDate := time.Date(2018, time.April, 11, 12, 13, 4, 95238e3, time.UTC) + actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC() + assert.Equal(t, actualDate, expectedDate) + + expectedSubType := "" + assert.Equal(t, resultAnchor.SubType(), expectedSubType) + + expectedValue := "YOTI_ADMIN" + assert.Equal(t, resultAnchor.Value(), expectedValue) + + actualSerialNo := resultAnchor.OriginServerCerts()[0].SerialNumber + assertServerCertSerialNo(t, "256616937783084706710155170893983549581", actualSerialNo) +} + +func TestAnchors_None(t *testing.T) { + var anchorSlice []*Anchor + + sources := GetSources(anchorSlice) + assert.Equal(t, len(sources), 0, "GetSources should not return anything with empty anchors") + + verifiers := GetVerifiers(anchorSlice) + assert.Equal(t, len(verifiers), 0, "GetVerifiers should not return anything with empty anchors") +} + +func TestAnchorParser_InvalidSignedTimestamp(t *testing.T) { + var protoAnchors []*yotiprotoattr.Anchor + protoAnchors = append(protoAnchors, &yotiprotoattr.Anchor{ + SignedTimeStamp: []byte("invalidProto"), + }) + parsedAnchors := ParseAnchors(protoAnchors) + + var expectedAnchors []*Anchor + assert.DeepEqual(t, expectedAnchors, parsedAnchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchors.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchors.go new file mode 100644 index 0000000..839a6e1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchors.go @@ -0,0 +1,105 @@ +package anchor + +import ( + "crypto/x509" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" +) + +// Anchor is the metadata associated with an attribute. It describes how an attribute has been provided +// to Yoti (SOURCE Anchor) and how it has been verified (VERIFIER Anchor). +// If an attribute has only one SOURCE Anchor with the value set to +// "USER_PROVIDED" and zero VERIFIER Anchors, then the attribute +// is a self-certified one. +type Anchor struct { + anchorType Type + originServerCerts []*x509.Certificate + signedTimestamp SignedTimestamp + subtype string + value string +} + +func newAnchor(anchorType Type, originServerCerts []*x509.Certificate, signedTimestamp *yotiprotocom.SignedTimestamp, subtype string, value string) *Anchor { + return &Anchor{ + anchorType: anchorType, + originServerCerts: originServerCerts, + signedTimestamp: convertSignedTimestamp(signedTimestamp), + subtype: subtype, + value: value, + } +} + +// Type Anchor type, based on the Object Identifier (OID) +type Type int + +const ( + // TypeUnknown - default value + TypeUnknown Type = 1 + iota + // TypeSource - how the anchor has been sourced + TypeSource + // TypeVerifier - how the anchor has been verified + TypeVerifier +) + +// Type of the Anchor - most likely either SOURCE or VERIFIER, but it's +// possible that new Anchor types will be added in future. +func (a Anchor) Type() Type { + return a.anchorType +} + +// OriginServerCerts are the X.509 certificate chain(DER-encoded ASN.1) +// from the service that assigned the attribute. +// +// The first certificate in the chain holds the public key that can be +// used to verify the Signature field; any following entries (zero or +// more) are for intermediate certificate authorities (in order). +// +// The last certificate in the chain must be verified against the Yoti root +// CA certificate. An extension in the first certificate holds the main artifact type, +// e.g. “PASSPORT”, which can be retrieved with .Value(). +func (a Anchor) OriginServerCerts() []*x509.Certificate { + return a.originServerCerts +} + +// SignedTimestamp is the time at which the signature was created. The +// message associated with the timestamp is the marshaled form of +// AttributeSigning (i.e. the same message that is signed in the +// Signature field). This method returns the SignedTimestamp +// object, the actual timestamp as a *time.Time can be called with +// .Timestamp() on the result of this function. +func (a Anchor) SignedTimestamp() SignedTimestamp { + return a.signedTimestamp +} + +// SubType is an indicator of any specific processing method, or +// subcategory, pertaining to an artifact. For example, for a passport, this would be +// either "NFC" or "OCR". +func (a Anchor) SubType() string { + return a.subtype +} + +// Value identifies the provider that either sourced or verified the attribute value. +// The range of possible values is not limited. For a SOURCE anchor, expect a value like +// PASSPORT, DRIVING_LICENSE. For a VERIFIER anchor, expect a value like YOTI_ADMIN. +func (a Anchor) Value() string { + return a.value +} + +// GetSources returns the anchors which identify how and when an attribute value was acquired. +func GetSources(anchors []*Anchor) (sources []*Anchor) { + return filterAnchors(anchors, TypeSource) +} + +// GetVerifiers returns the anchors which identify how and when an attribute value was verified by another provider. +func GetVerifiers(anchors []*Anchor) (sources []*Anchor) { + return filterAnchors(anchors, TypeVerifier) +} + +func filterAnchors(anchors []*Anchor, anchorType Type) (result []*Anchor) { + for _, v := range anchors { + if v.anchorType == anchorType { + result = append(result, v) + } + } + return result +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchors_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchors_test.go new file mode 100644 index 0000000..ed5287e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/anchors_test.go @@ -0,0 +1,20 @@ +package anchor + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestFilterAnchors_FilterSources(t *testing.T) { + anchorSlice := []*Anchor{ + {subtype: "a", anchorType: TypeSource}, + {subtype: "b", anchorType: TypeVerifier}, + {subtype: "c", anchorType: TypeSource}, + } + sources := filterAnchors(anchorSlice, TypeSource) + assert.Equal(t, len(sources), 2) + assert.Equal(t, sources[0].subtype, "a") + assert.Equal(t, sources[1].subtype, "c") + +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/signed_timestamp.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/signed_timestamp.go new file mode 100644 index 0000000..2081b7d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/anchor/signed_timestamp.go @@ -0,0 +1,35 @@ +package anchor + +import ( + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" +) + +// SignedTimestamp is the object which contains a timestamp +type SignedTimestamp struct { + version int32 + timestamp *time.Time +} + +func convertSignedTimestamp(protoSignedTimestamp *yotiprotocom.SignedTimestamp) SignedTimestamp { + uintTimestamp := protoSignedTimestamp.Timestamp + intTimestamp := int64(uintTimestamp) + unixTime := time.Unix(intTimestamp/1e6, (intTimestamp%1e6)*1e3) + + return SignedTimestamp{ + version: protoSignedTimestamp.Version, + timestamp: &unixTime, + } +} + +// Version indicates both the version of the protobuf message in use, +// as well as the specific hash algorithms. +func (s SignedTimestamp) Version() int32 { + return s.version +} + +// Timestamp is a point in time, to the nearest microsecond. +func (s SignedTimestamp) Timestamp() *time.Time { + return s.timestamp +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/attribute_details.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/attribute_details.go new file mode 100644 index 0000000..a380150 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/attribute_details.go @@ -0,0 +1,48 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" +) + +// attributeDetails is embedded in each attribute for fields common to all +// attributes +type attributeDetails struct { + name string + contentType string + anchors []*anchor.Anchor + id *string +} + +// Name gets the attribute name +func (a attributeDetails) Name() string { + return a.name +} + +// ID gets the attribute ID +func (a attributeDetails) ID() *string { + return a.id +} + +// ContentType gets the attribute's content type description +func (a attributeDetails) ContentType() string { + return a.contentType +} + +// Anchors are the metadata associated with an attribute. They describe +// how an attribute has been provided to Yoti (SOURCE Anchor) and how +// it has been verified (VERIFIER Anchor). +func (a attributeDetails) Anchors() []*anchor.Anchor { + return a.anchors +} + +// Sources returns the anchors which identify how and when an attribute value +// was acquired. +func (a attributeDetails) Sources() []*anchor.Anchor { + return anchor.GetSources(a.anchors) +} + +// Verifiers returns the anchors which identify how and when an attribute value +// was verified by another provider. +func (a attributeDetails) Verifiers() []*anchor.Anchor { + return anchor.GetVerifiers(a.anchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/attribute_test.go new file mode 100644 index 0000000..67b6c2b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/attribute_test.go @@ -0,0 +1,36 @@ +package attribute + +import ( + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestNewThirdPartyAttribute(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_third_party.txt") + + stringAttribute := NewString(protoAttribute) + + assert.Equal(t, stringAttribute.Value(), "test-third-party-attribute-0") + assert.Equal(t, stringAttribute.Name(), "com.thirdparty.id") + + assert.Equal(t, stringAttribute.Sources()[0].Value(), "THIRD_PARTY") + assert.Equal(t, stringAttribute.Sources()[0].SubType(), "orgName") + + assert.Equal(t, stringAttribute.Verifiers()[0].Value(), "THIRD_PARTY") + assert.Equal(t, stringAttribute.Verifiers()[0].SubType(), "orgName") +} + +func TestAttribute_DateOfBirth(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_date_of_birth.txt") + + dateOfBirthAttribute, err := NewDate(protoAttribute) + + assert.NilError(t, err) + + expectedDateOfBirth := time.Date(1970, time.December, 01, 0, 0, 0, 0, time.UTC) + actualDateOfBirth := dateOfBirthAttribute.Value() + + assert.Assert(t, actualDateOfBirth.Equal(expectedDateOfBirth)) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/date_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/date_attribute.go new file mode 100644 index 0000000..cdc55ce --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/date_attribute.go @@ -0,0 +1,39 @@ +package attribute + +import ( + "time" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// DateAttribute is a Yoti attribute which returns a date as *time.Time for its value +type DateAttribute struct { + attributeDetails + value *time.Time +} + +// NewDate creates a new Date attribute +func NewDate(a *yotiprotoattr.Attribute) (*DateAttribute, error) { + parsedTime, err := time.Parse("2006-01-02", string(a.Value)) + if err != nil { + return nil, err + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &DateAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: &parsedTime, + }, nil +} + +// Value returns the value of the TimeAttribute as *time.Time +func (a *DateAttribute) Value() *time.Time { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/date_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/date_attribute_test.go new file mode 100644 index 0000000..24807c9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/date_attribute_test.go @@ -0,0 +1,44 @@ +package attribute + +import ( + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestTimeAttribute_NewDate_DateOnly(t *testing.T) { + proto := yotiprotoattr.Attribute{ + Value: []byte("2011-12-25"), + } + + timeAttribute, err := NewDate(&proto) + assert.NilError(t, err) + + assert.Equal(t, *timeAttribute.Value(), time.Date(2011, 12, 25, 0, 0, 0, 0, time.UTC)) +} + +func TestTimeAttribute_DateOfBirth(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_date_of_birth.txt") + + dateOfBirthAttribute, err := NewDate(protoAttribute) + + assert.NilError(t, err) + + expectedDateOfBirth := time.Date(1970, time.December, 01, 0, 0, 0, 0, time.UTC) + actualDateOfBirth := dateOfBirthAttribute.Value() + + assert.Assert(t, actualDateOfBirth.Equal(expectedDateOfBirth)) +} + +func TestNewTime_ShouldReturnErrorForInvalidDate(t *testing.T) { + proto := yotiprotoattr.Attribute{ + Name: "example", + Value: []byte("2006-60-20"), + ContentType: yotiprotoattr.ContentType_DATE, + } + attribute, err := NewDate(&proto) + assert.Check(t, attribute == nil) + assert.ErrorContains(t, err, "month out of range") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/definition.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/definition.go new file mode 100644 index 0000000..b0d4b8a --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/definition.go @@ -0,0 +1,31 @@ +package attribute + +import ( + "encoding/json" +) + +// Definition contains information about the attribute(s) issued by a third party. +type Definition struct { + name string +} + +// Name of the attribute to be issued. +func (a Definition) Name() string { + return a.name +} + +// MarshalJSON returns encoded json +func (a Definition) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Name string `json:"name"` + }{ + Name: a.name, + }) +} + +// NewAttributeDefinition returns a new AttributeDefinition +func NewAttributeDefinition(s string) Definition { + return Definition{ + name: s, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/definition_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/definition_test.go new file mode 100644 index 0000000..b209e02 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/definition_test.go @@ -0,0 +1,18 @@ +package attribute + +import ( + "encoding/json" + "fmt" +) + +func ExampleDefinition_MarshalJSON() { + exampleDefinition := NewAttributeDefinition("exampleDefinition") + marshalledJSON, err := json.Marshal(exampleDefinition) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"exampleDefinition"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/document_details_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/document_details_attribute.go new file mode 100644 index 0000000..a18ccab --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/document_details_attribute.go @@ -0,0 +1,87 @@ +package attribute + +import ( + "fmt" + "strings" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +const ( + documentDetailsDateFormatConst = "2006-01-02" +) + +// DocumentDetails represents information extracted from a document provided by the user +type DocumentDetails struct { + DocumentType string + IssuingCountry string + DocumentNumber string + ExpirationDate *time.Time + IssuingAuthority string +} + +// DocumentDetailsAttribute wraps a document details with anchor data +type DocumentDetailsAttribute struct { + attributeDetails + value DocumentDetails +} + +// Value returns the document details struct attached to this attribute +func (attr *DocumentDetailsAttribute) Value() DocumentDetails { + return attr.value +} + +// NewDocumentDetails creates a DocumentDetailsAttribute which wraps a +// DocumentDetails with anchor data +func NewDocumentDetails(a *yotiprotoattr.Attribute) (*DocumentDetailsAttribute, error) { + parsedAnchors := anchor.ParseAnchors(a.Anchors) + details := DocumentDetails{} + err := details.Parse(string(a.Value)) + if err != nil { + return nil, err + } + + return &DocumentDetailsAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: details, + }, nil +} + +// Parse fills a DocumentDetails object from a raw string +func (details *DocumentDetails) Parse(data string) error { + dataSlice := strings.Split(data, " ") + + if len(dataSlice) < 3 { + return fmt.Errorf("Document Details data is invalid, %s", data) + } + for _, section := range dataSlice { + if section == "" { + return fmt.Errorf("Document Details data is invalid %s", data) + } + } + + details.DocumentType = dataSlice[0] + details.IssuingCountry = dataSlice[1] + details.DocumentNumber = dataSlice[2] + if len(dataSlice) > 3 && dataSlice[3] != "-" { + expirationDateData, dateErr := time.Parse(documentDetailsDateFormatConst, dataSlice[3]) + + if dateErr == nil { + details.ExpirationDate = &expirationDateData + } else { + return dateErr + } + } + if len(dataSlice) > 4 { + details.IssuingAuthority = dataSlice[4] + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/document_details_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/document_details_attribute_test.go new file mode 100644 index 0000000..bf2e7de --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/document_details_attribute_test.go @@ -0,0 +1,185 @@ +package attribute + +import ( + "fmt" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func ExampleDocumentDetails_Parse() { + raw := "PASSPORT GBR 1234567 2022-09-12" + details := DocumentDetails{} + err := details.Parse(raw) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Printf( + "Document Type: %s, Issuing Country: %s, Document Number: %s, Expiration Date: %s", + details.DocumentType, + details.IssuingCountry, + details.DocumentNumber, + details.ExpirationDate, + ) + // Output: Document Type: PASSPORT, Issuing Country: GBR, Document Number: 1234567, Expiration Date: 2022-09-12 00:00:00 +0000 UTC +} + +func ExampleNewDocumentDetails() { + proto := yotiprotoattr.Attribute{ + Name: "exampleDocumentDetails", + Value: []byte("PASSPORT GBR 1234567 2022-09-12"), + ContentType: yotiprotoattr.ContentType_STRING, + } + attribute, err := NewDocumentDetails(&proto) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Printf( + "Document Type: %s, With %d Anchors", + attribute.Value().DocumentType, + len(attribute.Anchors()), + ) + // Output: Document Type: PASSPORT, With 0 Anchors +} + +func TestDocumentDetailsShouldParseDrivingLicenceWithoutExpiry(t *testing.T) { + drivingLicenceGBR := "PASS_CARD GBR 1234abc - DVLA" + + details := DocumentDetails{} + err := details.Parse(drivingLicenceGBR) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "PASS_CARD") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Assert(t, details.ExpirationDate == nil) + assert.Equal(t, details.IssuingCountry, "GBR") + assert.Equal(t, details.IssuingAuthority, "DVLA") +} + +func TestDocumentDetailsShouldParseRedactedAadhar(t *testing.T) { + aadhaar := "AADHAAR IND ****1234 2016-05-01" + details := DocumentDetails{} + err := details.Parse(aadhaar) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "AADHAAR") + assert.Equal(t, details.DocumentNumber, "****1234") + assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01") + assert.Equal(t, details.IssuingCountry, "IND") + assert.Equal(t, details.IssuingAuthority, "") +} + +func TestDocumentDetailsShouldParseSpecialCharacters(t *testing.T) { + testData := [][]string{ + {"type country **** - authority", "****"}, + {"type country ~!@#$%^&*()-_=+[]{}|;':,./<>? - authority", "~!@#$%^&*()-_=+[]{}|;':,./<>?"}, + {"type country \"\" - authority", "\"\""}, + {"type country \\ - authority", "\\"}, + {"type country \" - authority", "\""}, + {"type country '' - authority", "''"}, + {"type country ' - authority", "'"}, + } + for _, row := range testData { + details := DocumentDetails{} + err := details.Parse(row[0]) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentNumber, row[1]) + } +} + +func TestDocumentDetailsShouldFailOnDoubleSpace(t *testing.T) { + data := "AADHAAR IND ****1234" + details := DocumentDetails{} + err := details.Parse(data) + assert.Check(t, err != nil) + assert.ErrorContains(t, err, "Document Details data is invalid") +} + +func TestDocumentDetailsShouldParseDrivingLicenceWithExtraAttribute(t *testing.T) { + drivingLicenceGBR := "DRIVING_LICENCE GBR 1234abc 2016-05-01 DVLA someThirdAttribute" + details := DocumentDetails{} + err := details.Parse(drivingLicenceGBR) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "DRIVING_LICENCE") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01") + assert.Equal(t, details.IssuingCountry, "GBR") + assert.Equal(t, details.IssuingAuthority, "DVLA") +} + +func TestDocumentDetailsShouldParseDrivingLicenceWithAllOptionalAttributes(t *testing.T) { + drivingLicenceGBR := "DRIVING_LICENCE GBR 1234abc 2016-05-01 DVLA" + + details := DocumentDetails{} + err := details.Parse(drivingLicenceGBR) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "DRIVING_LICENCE") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01") + assert.Equal(t, details.IssuingCountry, "GBR") + assert.Equal(t, details.IssuingAuthority, "DVLA") +} + +func TestDocumentDetailsShouldParseAadhaar(t *testing.T) { + aadhaar := "AADHAAR IND 1234abc 2016-05-01" + + details := DocumentDetails{} + err := details.Parse(aadhaar) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "AADHAAR") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01") + assert.Equal(t, details.IssuingCountry, "IND") +} + +func TestDocumentDetailsShouldParsePassportWithMandatoryFieldsOnly(t *testing.T) { + passportGBR := "PASSPORT GBR 1234abc" + + details := DocumentDetails{} + err := details.Parse(passportGBR) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "PASSPORT") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Assert(t, details.ExpirationDate == nil) + assert.Equal(t, details.IssuingCountry, "GBR") + assert.Equal(t, details.IssuingAuthority, "") +} + +func TestDocumentDetailsShouldErrorOnEmptyString(t *testing.T) { + empty := "" + + details := DocumentDetails{} + err := details.Parse(empty) + assert.ErrorContains(t, err, "Document Details data is invalid") +} + +func TestDocumentDetailsShouldErrorIfLessThan3Words(t *testing.T) { + corrupt := "PASS_CARD GBR" + details := DocumentDetails{} + err := details.Parse(corrupt) + assert.ErrorContains(t, err, "Document Details data is invalid") +} + +func TestDocumentDetailsShouldErrorForInvalidExpirationDate(t *testing.T) { + corrupt := "PASSPORT GBR 1234abc X016-05-01" + details := DocumentDetails{} + err := details.Parse(corrupt) + assert.ErrorContains(t, err, "cannot parse") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/generic_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/generic_attribute.go new file mode 100644 index 0000000..c729e30 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/generic_attribute.go @@ -0,0 +1,38 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// GenericAttribute is a Yoti attribute which returns a generic value +type GenericAttribute struct { + attributeDetails + value interface{} +} + +// NewGeneric creates a new generic attribute +func NewGeneric(a *yotiprotoattr.Attribute) *GenericAttribute { + value, err := parseValue(a.ContentType, a.Value) + + if err != nil { + return nil + } + + var parsedAnchors = anchor.ParseAnchors(a.Anchors) + + return &GenericAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: value, + } +} + +// Value returns the value of the GenericAttribute as an interface +func (a *GenericAttribute) Value() interface{} { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/generic_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/generic_attribute_test.go new file mode 100644 index 0000000..e2daae8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/generic_attribute_test.go @@ -0,0 +1,39 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestNewGeneric_ShouldParseUnknownTypeAsString(t *testing.T) { + value := []byte("value") + protoAttr := yotiprotoattr.Attribute{ + ContentType: yotiprotoattr.ContentType_UNDEFINED, + Value: value, + } + parsed := NewGeneric(&protoAttr) + + stringValue, ok := parsed.Value().(string) + assert.Check(t, ok) + + assert.Equal(t, stringValue, string(value)) +} + +func TestGeneric_ContentType(t *testing.T) { + attribute := GenericAttribute{ + attributeDetails: attributeDetails{ + contentType: "contentType", + }, + } + + assert.Equal(t, attribute.ContentType(), "contentType") +} + +func TestNewGeneric_ShouldReturnNilForInvalidProtobuf(t *testing.T) { + invalid := NewGeneric(&yotiprotoattr.Attribute{ + ContentType: yotiprotoattr.ContentType_JSON, + }) + assert.Check(t, invalid == nil) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/helper_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/helper_test.go new file mode 100644 index 0000000..47c28ea --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/helper_test.go @@ -0,0 +1,21 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" +) + +func createAttributeFromTestFile(t *testing.T, filename string) *yotiprotoattr.Attribute { + attributeBytes := test.DecodeTestFile(t, filename) + + attributeStruct := &yotiprotoattr.Attribute{} + + err2 := proto.Unmarshal(attributeBytes, attributeStruct) + assert.NilError(t, err2) + + return attributeStruct +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_attribute.go new file mode 100644 index 0000000..fd9d7f1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_attribute.go @@ -0,0 +1,53 @@ +package attribute + +import ( + "errors" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// ImageAttribute is a Yoti attribute which returns an image as its value +type ImageAttribute struct { + attributeDetails + value media.Media +} + +// NewImage creates a new Image attribute +func NewImage(a *yotiprotoattr.Attribute) (*ImageAttribute, error) { + imageValue, err := parseImageValue(a.ContentType, a.Value) + if err != nil { + return nil, err + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &ImageAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: imageValue, + }, nil +} + +// Value returns the value of the ImageAttribute as media.Media +func (a *ImageAttribute) Value() media.Media { + return a.value +} + +func parseImageValue(contentType yotiprotoattr.ContentType, byteValue []byte) (media.Media, error) { + switch contentType { + case yotiprotoattr.ContentType_JPEG: + return media.JPEGImage(byteValue), nil + + case yotiprotoattr.ContentType_PNG: + return media.PNGImage(byteValue), nil + + default: + return nil, errors.New("cannot create Image with unsupported type") + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_attribute_test.go new file mode 100644 index 0000000..2fe620f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_attribute_test.go @@ -0,0 +1,106 @@ +package attribute + +import ( + "encoding/base64" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestImageAttribute_Image_Png(t *testing.T) { + attributeName := consts.AttrSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + assert.DeepEqual(t, selfie.Value().Data(), byteValue) +} + +func TestImageAttribute_Image_Jpeg(t *testing.T) { + attributeName := consts.AttrSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + assert.DeepEqual(t, selfie.Value().Data(), byteValue) +} + +func TestImageAttribute_Image_Default(t *testing.T) { + attributeName := consts.AttrSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + assert.DeepEqual(t, selfie.Value().Data(), byteValue) +} + +func TestImageAttribute_Base64Selfie_Png(t *testing.T) { + attributeName := consts.AttrSelfie + imageBytes := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: imageBytes, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(imageBytes) + + expectedBase64Selfie := "data:image/png;base64," + base64ImageExpectedValue + + base64Selfie := selfie.Value().Base64URL() + + assert.Equal(t, base64Selfie, expectedBase64Selfie) +} + +func TestImageAttribute_Base64URL_Jpeg(t *testing.T) { + attributeName := consts.AttrSelfie + imageBytes := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: imageBytes, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(imageBytes) + + expectedBase64Selfie := "data:image/jpeg;base64," + base64ImageExpectedValue + + base64Selfie := selfie.Value().Base64URL() + + assert.Equal(t, base64Selfie, expectedBase64Selfie) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_slice_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_slice_attribute.go new file mode 100644 index 0000000..de507ab --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_slice_attribute.go @@ -0,0 +1,69 @@ +package attribute + +import ( + "errors" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// ImageSliceAttribute is a Yoti attribute which returns a slice of images as its value +type ImageSliceAttribute struct { + attributeDetails + value []media.Media +} + +// NewImageSlice creates a new ImageSlice attribute +func NewImageSlice(a *yotiprotoattr.Attribute) (*ImageSliceAttribute, error) { + if a.ContentType != yotiprotoattr.ContentType_MULTI_VALUE { + return nil, errors.New("creating an Image Slice attribute with content types other than MULTI_VALUE is not supported") + } + + parsedMultiValue, err := parseMultiValue(a.Value) + + if err != nil { + return nil, err + } + + var imageSliceValue []media.Media + if parsedMultiValue != nil { + imageSliceValue, err = CreateImageSlice(parsedMultiValue) + if err != nil { + return nil, err + } + } + + return &ImageSliceAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: anchor.ParseAnchors(a.Anchors), + id: &a.EphemeralId, + }, + value: imageSliceValue, + }, nil +} + +// CreateImageSlice takes a slice of Items, and converts them into a slice of images +func CreateImageSlice(items []*Item) (result []media.Media, err error) { + for _, item := range items { + + switch i := item.Value.(type) { + case media.PNGImage: + result = append(result, i) + case media.JPEGImage: + result = append(result, i) + default: + return nil, fmt.Errorf("unexpected item type %T", i) + } + } + + return result, nil +} + +// Value returns the value of the ImageSliceAttribute +func (a *ImageSliceAttribute) Value() []media.Media { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_slice_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_slice_attribute_test.go new file mode 100644 index 0000000..2c30092 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/image_slice_attribute_test.go @@ -0,0 +1,61 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func assertIsExpectedImage(t *testing.T, image media.Media, imageMIMEType string, expectedBase64URLLast10 string) { + assert.Equal(t, image.MIME(), imageMIMEType) + + actualBase64URL := image.Base64URL() + + ActualBase64URLLast10Chars := actualBase64URL[len(actualBase64URL)-10:] + + assert.Equal(t, ActualBase64URLLast10Chars, expectedBase64URLLast10) +} + +func assertIsExpectedDocumentImagesAttribute(t *testing.T, actualDocumentImages []media.Media, anchor *anchor.Anchor) { + + assert.Equal(t, len(actualDocumentImages), 2, "This Document Images attribute should have two images") + + assertIsExpectedImage(t, actualDocumentImages[0], media.ImageTypeJPEG, "vWgD//2Q==") + assertIsExpectedImage(t, actualDocumentImages[1], media.ImageTypeJPEG, "38TVEH/9k=") + + expectedValue := "NATIONAL_ID" + assert.Equal(t, anchor.Value(), expectedValue) + + expectedSubType := "STATE_ID" + assert.Equal(t, anchor.SubType(), expectedSubType) +} + +func TestAttribute_NewImageSlice(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt") + + documentImagesAttribute, err := NewImageSlice(protoAttribute) + + assert.NilError(t, err) + + assertIsExpectedDocumentImagesAttribute(t, documentImagesAttribute.Value(), documentImagesAttribute.Anchors()[0]) +} + +func TestAttribute_ImageSliceNotCreatedWithNonMultiValueType(t *testing.T) { + attributeName := "attributeName" + attributeValueString := "value" + attributeValue := []byte(attributeValueString) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + _, err := NewImageSlice(attr) + + assert.Assert(t, err != nil, "Expected error when creating image slice from attribute which isn't of multi-value type") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/issuance_details.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/issuance_details.go new file mode 100644 index 0000000..381d4e9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/issuance_details.go @@ -0,0 +1,86 @@ +package attribute + +import ( + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare" + "google.golang.org/protobuf/proto" +) + +// IssuanceDetails contains information about the attribute(s) issued by a third party +type IssuanceDetails struct { + token string + expiryDate *time.Time + attributes []Definition +} + +// Token is the issuance token that can be used to retrieve the user's stored details. +// These details will be used to issue attributes on behalf of an organisation to that user. +func (i IssuanceDetails) Token() string { + return i.token +} + +// ExpiryDate is the timestamp at which the request for the attribute value +// from third party will expire. Will be nil if not provided. +func (i IssuanceDetails) ExpiryDate() *time.Time { + return i.expiryDate +} + +// Attributes information about the attributes the third party would like to issue. +func (i IssuanceDetails) Attributes() []Definition { + return i.attributes +} + +// ParseIssuanceDetails takes the Third Party Attribute object and converts it into an IssuanceDetails struct +func ParseIssuanceDetails(thirdPartyAttributeBytes []byte) (*IssuanceDetails, error) { + thirdPartyAttributeStruct := &yotiprotoshare.ThirdPartyAttribute{} + if err := proto.Unmarshal(thirdPartyAttributeBytes, thirdPartyAttributeStruct); err != nil { + return nil, fmt.Errorf("unable to parse ThirdPartyAttribute value: %q. Error: %q", string(thirdPartyAttributeBytes), err) + } + + var issuingAttributesProto = thirdPartyAttributeStruct.GetIssuingAttributes() + var issuingAttributeDefinitions = parseIssuingAttributeDefinitions(issuingAttributesProto.GetDefinitions()) + + expiryDate, dateParseErr := parseExpiryDate(issuingAttributesProto.ExpiryDate) + + var issuanceTokenBytes = thirdPartyAttributeStruct.GetIssuanceToken() + + if len(issuanceTokenBytes) == 0 { + return nil, errors.New("Issuance Token is invalid") + } + + base64EncodedToken := base64.StdEncoding.EncodeToString(issuanceTokenBytes) + + return &IssuanceDetails{ + token: base64EncodedToken, + expiryDate: expiryDate, + attributes: issuingAttributeDefinitions, + }, dateParseErr +} + +func parseIssuingAttributeDefinitions(definitions []*yotiprotoshare.Definition) (issuingAttributes []Definition) { + for _, definition := range definitions { + attributeDefinition := Definition{ + name: definition.Name, + } + issuingAttributes = append(issuingAttributes, attributeDefinition) + } + + return issuingAttributes +} + +func parseExpiryDate(expiryDateString string) (*time.Time, error) { + if expiryDateString == "" { + return nil, nil + } + + parsedTime, err := time.Parse(time.RFC3339Nano, expiryDateString) + if err != nil { + return nil, err + } + + return &parsedTime, err +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/issuance_details_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/issuance_details_test.go new file mode 100644 index 0000000..462d863 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/issuance_details_test.go @@ -0,0 +1,145 @@ +package attribute + +import ( + "encoding/base64" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare" + "google.golang.org/protobuf/proto" + + "gotest.tools/v3/assert" + + is "gotest.tools/v3/assert/cmp" +) + +func TestShouldParseThirdPartyAttributeCorrectly(t *testing.T) { + var thirdPartyAttributeBytes = test.GetTestFileBytes(t, "../../test/fixtures/test_third_party_issuance_details.txt") + issuanceDetails, err := ParseIssuanceDetails(thirdPartyAttributeBytes) + + assert.NilError(t, err) + assert.Equal(t, issuanceDetails.Attributes()[0].Name(), "com.thirdparty.id") + assert.Equal(t, issuanceDetails.Token(), "c29tZUlzc3VhbmNlVG9rZW4=") + assert.Equal(t, + issuanceDetails.ExpiryDate().Format("2006-01-02T15:04:05.000Z"), + "2019-10-15T22:04:05.123Z") +} + +func TestShouldLogWarningIfErrorInParsingExpiryDate(t *testing.T) { + var tokenValue = "41548a175dfaw" + thirdPartyAttribute := &yotiprotoshare.ThirdPartyAttribute{ + IssuanceToken: []byte(tokenValue), + IssuingAttributes: &yotiprotoshare.IssuingAttributes{ + ExpiryDate: "2006-13-02T15:04:05.000Z", + }, + } + + marshalled, err := proto.Marshal(thirdPartyAttribute) + + assert.NilError(t, err) + + var tokenBytes = []byte(tokenValue) + var expectedBase64Token = base64.StdEncoding.EncodeToString(tokenBytes) + + result, err := ParseIssuanceDetails(marshalled) + assert.Equal(t, expectedBase64Token, result.Token()) + assert.Assert(t, is.Nil(result.ExpiryDate())) + assert.Equal(t, "parsing time \"2006-13-02T15:04:05.000Z\": month out of range", err.Error()) +} + +func TestIssuanceDetails_parseExpiryDate_ShouldParseAllRFC3339Formats(t *testing.T) { + table := []struct { + Input string + Expected time.Time + }{ + { + Input: "2006-01-02T22:04:05Z", + Expected: time.Date(2006, 01, 02, 22, 4, 5, 0, time.UTC), + }, + { + Input: "2010-05-20T10:44:25Z", + Expected: time.Date(2010, 5, 20, 10, 44, 25, 0, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.1Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 100e6, time.UTC), + }, + { + Input: "2012-03-06T04:20:07.5Z", + Expected: time.Date(2012, 3, 6, 4, 20, 7, 500e6, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.12Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 120e6, time.UTC), + }, + { + Input: "2013-03-04T20:43:55.56Z", + Expected: time.Date(2013, 3, 4, 20, 43, 55, 560e6, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.123Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 123e6, time.UTC), + }, + { + Input: "2007-04-07T17:34:11.784Z", + Expected: time.Date(2007, 4, 7, 17, 34, 11, 784e6, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.1234Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 123400e3, time.UTC), + }, + { + Input: "2017-09-14T16:54:30.4784Z", + Expected: time.Date(2017, 9, 14, 16, 54, 30, 478400e3, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.12345Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 123450e3, time.UTC), + }, + { + Input: "2009-06-07T14:20:30.74622Z", + Expected: time.Date(2009, 6, 7, 14, 20, 30, 746220e3, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.123456Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 123456e3, time.UTC), + }, + { + Input: "2008-10-25T06:50:55.643562Z", + Expected: time.Date(2008, 10, 25, 6, 50, 55, 643562e3, time.UTC), + }, + { + Input: "2002-10-02T10:00:00-05:00", + Expected: time.Date(2002, 10, 2, 10, 0, 0, 0, time.FixedZone("-0500", -5*60*60)), + }, + { + Input: "2002-10-02T10:00:00+11:00", + Expected: time.Date(2002, 10, 2, 10, 0, 0, 0, time.FixedZone("+1100", 11*60*60)), + }, + { + Input: "1920-03-13T19:50:53.999999Z", + Expected: time.Date(1920, 3, 13, 19, 50, 53, 999999e3, time.UTC), + }, + { + Input: "1920-03-13T19:50:54.000001Z", + Expected: time.Date(1920, 3, 13, 19, 50, 54, 1e3, time.UTC), + }, + } + + for _, row := range table { + func(input string, expected time.Time) { + expiryDate, err := parseExpiryDate(input) + assert.NilError(t, err) + assert.Equal(t, expiryDate.UTC(), expected.UTC()) + }(row.Input, row.Expected) + } +} + +func TestInvalidProtobufThrowsError(t *testing.T) { + result, err := ParseIssuanceDetails([]byte("invalid")) + + assert.Assert(t, is.Nil(result)) + + assert.ErrorContains(t, err, "unable to parse ThirdPartyAttribute value") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/item.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/item.go new file mode 100644 index 0000000..3efd2b9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/item.go @@ -0,0 +1,14 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// Item is a structure which contains information about an attribute value +type Item struct { + // ContentType is the content of the item. + ContentType yotiprotoattr.ContentType + + // Value is the underlying data of the item. + Value interface{} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/json_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/json_attribute.go new file mode 100644 index 0000000..be40920 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/json_attribute.go @@ -0,0 +1,58 @@ +package attribute + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// JSONAttribute is a Yoti attribute which returns an interface as its value +type JSONAttribute struct { + attributeDetails + // value returns the value of a JSON attribute in the form of an interface + value map[string]interface{} +} + +// NewJSON creates a new JSON attribute +func NewJSON(a *yotiprotoattr.Attribute) (*JSONAttribute, error) { + var interfaceValue map[string]interface{} + decoder := json.NewDecoder(bytes.NewReader(a.Value)) + decoder.UseNumber() + err := decoder.Decode(&interfaceValue) + if err != nil { + err = fmt.Errorf("unable to parse JSON value: %q. Error: %q", a.Value, err) + return nil, err + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &JSONAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: interfaceValue, + }, nil +} + +// unmarshallJSON unmarshalls JSON into an interface +func unmarshallJSON(byteValue []byte) (result map[string]interface{}, err error) { + var unmarshalledJSON map[string]interface{} + err = json.Unmarshal(byteValue, &unmarshalledJSON) + + if err != nil { + return nil, err + } + + return unmarshalledJSON, err +} + +// Value returns the value of the JSONAttribute as an interface. +func (a *JSONAttribute) Value() map[string]interface{} { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/json_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/json_attribute_test.go new file mode 100644 index 0000000..4275637 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/json_attribute_test.go @@ -0,0 +1,76 @@ +package attribute + +import ( + "fmt" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func ExampleNewJSON() { + proto := yotiprotoattr.Attribute{ + Name: "exampleJSON", + Value: []byte(`{"foo":"bar"}`), + ContentType: yotiprotoattr.ContentType_JSON, + } + attribute, err := NewJSON(&proto) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + fmt.Println(attribute.Value()) + // Output: map[foo:bar] +} + +func TestNewJSON_ShouldReturnNilForInvalidJSON(t *testing.T) { + proto := yotiprotoattr.Attribute{ + Name: "exampleJSON", + Value: []byte("Not a json document"), + ContentType: yotiprotoattr.ContentType_JSON, + } + attribute, err := NewJSON(&proto) + assert.Check(t, attribute == nil) + assert.ErrorContains(t, err, "unable to parse JSON value") +} + +func TestYotiClient_UnmarshallJSONValue_InvalidValueThrowsError(t *testing.T) { + invalidStructuredAddress := []byte("invalidBool") + + _, err := unmarshallJSON(invalidStructuredAddress) + + assert.Assert(t, err != nil) +} + +func TestYotiClient_UnmarshallJSONValue_ValidValue(t *testing.T) { + const ( + countryIso = "IND" + nestedValue = "NestedValue" + ) + + var structuredAddress = []byte(` + { + "address_format": 2, + "building": "House No.86-A", + "state": "Punjab", + "postal_code": "141012", + "country_iso": "` + countryIso + `", + "country": "India", + "formatted_address": "House No.86-A\nRajgura Nagar\nLudhina\nPunjab\n141012\nIndia", + "1": + { + "1-1": + { + "1-1-1": "` + nestedValue + `" + } + } + } + `) + + parsedStructuredAddress, err := unmarshallJSON(structuredAddress) + assert.NilError(t, err, "Failed to parse structured address") + + actualCountryIso := parsedStructuredAddress["country_iso"] + + assert.Equal(t, countryIso, actualCountryIso) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/multivalue_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/multivalue_attribute.go new file mode 100644 index 0000000..926141f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/multivalue_attribute.go @@ -0,0 +1,90 @@ +package attribute + +import ( + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" +) + +// MultiValueAttribute is a Yoti attribute which returns a multi-valued attribute +type MultiValueAttribute struct { + attributeDetails + items []*Item +} + +// NewMultiValue creates a new MultiValue attribute +func NewMultiValue(a *yotiprotoattr.Attribute) (*MultiValueAttribute, error) { + attributeItems, err := parseMultiValue(a.Value) + + if err != nil { + return nil, err + } + + return &MultiValueAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: anchor.ParseAnchors(a.Anchors), + id: &a.EphemeralId, + }, + items: attributeItems, + }, nil +} + +// parseMultiValue recursively unmarshals and converts Multi Value bytes into a slice of Items +func parseMultiValue(data []byte) ([]*Item, error) { + var attributeItems []*Item + protoMultiValueStruct, err := unmarshallMultiValue(data) + + if err != nil { + return nil, err + } + + for _, multiValueItem := range protoMultiValueStruct.Values { + var value *Item + if multiValueItem.ContentType == yotiprotoattr.ContentType_MULTI_VALUE { + parsedInnerMultiValueItems, err := parseMultiValue(multiValueItem.Data) + + if err != nil { + return nil, fmt.Errorf("unable to parse multi-value data: %v", err) + } + + value = &Item{ + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Value: parsedInnerMultiValueItems, + } + } else { + itemValue, err := parseValue(multiValueItem.ContentType, multiValueItem.Data) + + if err != nil { + return nil, fmt.Errorf("unable to parse data within a multi-value attribute. Content type: %q, data: %q, error: %v", + multiValueItem.ContentType, multiValueItem.Data, err) + } + + value = &Item{ + ContentType: multiValueItem.ContentType, + Value: itemValue, + } + } + attributeItems = append(attributeItems, value) + } + + return attributeItems, nil +} + +func unmarshallMultiValue(bytes []byte) (*yotiprotoattr.MultiValue, error) { + multiValueStruct := &yotiprotoattr.MultiValue{} + + if err := proto.Unmarshal(bytes, multiValueStruct); err != nil { + return nil, fmt.Errorf("unable to parse MULTI_VALUE value: %q. Error: %q", string(bytes), err) + } + + return multiValueStruct, nil +} + +// Value returns the value of the MultiValueAttribute as a string +func (a *MultiValueAttribute) Value() []*Item { + return a.items +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/multivalue_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/multivalue_attribute_test.go new file mode 100644 index 0000000..15a24f9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/multivalue_attribute_test.go @@ -0,0 +1,157 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func marshallMultiValue(t *testing.T, multiValue *yotiprotoattr.MultiValue) []byte { + marshalled, err := proto.Marshal(multiValue) + + assert.NilError(t, err) + + return marshalled +} + +func createMultiValueAttribute(t *testing.T, multiValueItemSlice []*yotiprotoattr.MultiValue_Value) (*MultiValueAttribute, error) { + var multiValueStruct = &yotiprotoattr.MultiValue{ + Values: multiValueItemSlice, + } + + var marshalledMultiValueData = marshallMultiValue(t, multiValueStruct) + attributeName := "nestedMultiValue" + + var protoAttribute = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: marshalledMultiValueData, + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Anchors: []*yotiprotoattr.Anchor{}, + } + + return NewMultiValue(protoAttribute) +} + +func TestAttribute_MultiValueNotCreatedWithNonMultiValueType(t *testing.T) { + attributeName := "attributeName" + attributeValueString := "value" + attributeValue := []byte(attributeValueString) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + _, err := NewMultiValue(attr) + + assert.Assert(t, err != nil, "Expected error when creating multi value from attribute which isn't of multi-value type") +} + +func TestAttribute_NewMultiValue(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt") + + multiValueAttribute, err := NewMultiValue(protoAttribute) + + assert.NilError(t, err) + + documentImagesAttributeItems, err := CreateImageSlice(multiValueAttribute.Value()) + assert.NilError(t, err) + + assertIsExpectedDocumentImagesAttribute(t, documentImagesAttributeItems, multiValueAttribute.Anchors()[0]) +} + +func TestAttribute_InvalidMultiValueNotReturned(t *testing.T) { + var invalidMultiValueItem = &yotiprotoattr.MultiValue_Value{ + ContentType: yotiprotoattr.ContentType_DATE, + Data: []byte("invalid"), + } + + var stringMultiValueItem = &yotiprotoattr.MultiValue_Value{ + ContentType: yotiprotoattr.ContentType_STRING, + Data: []byte("string"), + } + + var multiValueItemSlice = []*yotiprotoattr.MultiValue_Value{invalidMultiValueItem, stringMultiValueItem} + + var multiValueStruct = &yotiprotoattr.MultiValue{ + Values: multiValueItemSlice, + } + + var marshalledMultiValueData = marshallMultiValue(t, multiValueStruct) + attributeName := "nestedMultiValue" + + var protoAttribute = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: marshalledMultiValueData, + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Anchors: []*yotiprotoattr.Anchor{}, + } + + multiValueAttr, err := NewMultiValue(protoAttribute) + assert.Check(t, err != nil) + + assert.Assert(t, is.Nil(multiValueAttr)) +} + +func TestAttribute_NestedMultiValue(t *testing.T) { + var innerMultiValueProtoValue = createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt").Value + + var stringMultiValueItem = &yotiprotoattr.MultiValue_Value{ + ContentType: yotiprotoattr.ContentType_STRING, + Data: []byte("string"), + } + + var multiValueItem = &yotiprotoattr.MultiValue_Value{ + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Data: innerMultiValueProtoValue, + } + + var multiValueItemSlice = []*yotiprotoattr.MultiValue_Value{stringMultiValueItem, multiValueItem} + + multiValueAttribute, err := createMultiValueAttribute(t, multiValueItemSlice) + + assert.NilError(t, err) + + for key, value := range multiValueAttribute.Value() { + switch key { + case 0: + value0 := value.Value + + assert.Equal(t, value0.(string), "string") + case 1: + value1 := value.Value + + innerItems, ok := value1.([]*Item) + assert.Assert(t, ok) + + for innerKey, item := range innerItems { + switch innerKey { + case 0: + assertIsExpectedImage(t, item.Value.(media.Media), media.ImageTypeJPEG, "vWgD//2Q==") + + case 1: + assertIsExpectedImage(t, item.Value.(media.Media), media.ImageTypeJPEG, "38TVEH/9k=") + } + } + } + } +} + +func TestAttribute_MultiValueGenericGetter(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt") + multiValueAttribute, err := NewMultiValue(protoAttribute) + assert.NilError(t, err) + + // We need to cast, since GetAttribute always returns generic attributes + multiValueAttributeValue := multiValueAttribute.Value() + imageSlice, err := CreateImageSlice(multiValueAttributeValue) + assert.NilError(t, err) + + assertIsExpectedDocumentImagesAttribute(t, imageSlice, multiValueAttribute.Anchors()[0]) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/parser.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/parser.go new file mode 100644 index 0000000..d663595 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/parser.go @@ -0,0 +1,56 @@ +package attribute + +import ( + "fmt" + "strconv" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +func parseValue(contentType yotiprotoattr.ContentType, byteValue []byte) (interface{}, error) { + switch contentType { + case yotiprotoattr.ContentType_DATE: + parsedTime, err := time.Parse("2006-01-02", string(byteValue)) + + if err == nil { + return &parsedTime, nil + } + + return nil, fmt.Errorf("unable to parse date value: %q. Error: %q", string(byteValue), err) + + case yotiprotoattr.ContentType_JSON: + unmarshalledJSON, err := unmarshallJSON(byteValue) + + if err == nil { + return unmarshalledJSON, nil + } + + return nil, fmt.Errorf("unable to parse JSON value: %q. Error: %q", string(byteValue), err) + + case yotiprotoattr.ContentType_STRING: + return string(byteValue), nil + + case yotiprotoattr.ContentType_MULTI_VALUE: + return parseMultiValue(byteValue) + + case yotiprotoattr.ContentType_INT: + var stringValue = string(byteValue) + intValue, err := strconv.Atoi(stringValue) + if err == nil { + return intValue, nil + } + + return nil, fmt.Errorf("unable to parse INT value: %q. Error: %q", string(byteValue), err) + + case yotiprotoattr.ContentType_JPEG, + yotiprotoattr.ContentType_PNG: + return parseImageValue(contentType, byteValue) + + case yotiprotoattr.ContentType_UNDEFINED: + return string(byteValue), nil + + default: + return string(byteValue), nil + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/parser_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/parser_test.go new file mode 100644 index 0000000..cc9f3d8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/parser_test.go @@ -0,0 +1,16 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestParseValue_ShouldParseInt(t *testing.T) { + parsed, err := parseValue(yotiprotoattr.ContentType_INT, []byte("7")) + assert.NilError(t, err) + integer, ok := parsed.(int) + assert.Check(t, ok) + assert.Equal(t, integer, 7) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/string_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/string_attribute.go new file mode 100644 index 0000000..73346b9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/string_attribute.go @@ -0,0 +1,32 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// StringAttribute is a Yoti attribute which returns a string as its value +type StringAttribute struct { + attributeDetails + value string +} + +// NewString creates a new String attribute +func NewString(a *yotiprotoattr.Attribute) *StringAttribute { + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &StringAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: string(a.Value), + } +} + +// Value returns the value of the StringAttribute as a string +func (a *StringAttribute) Value() string { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/string_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/string_attribute_test.go new file mode 100644 index 0000000..828df20 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/attribute/string_attribute_test.go @@ -0,0 +1,22 @@ +package attribute + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestStringAttribute_NewThirdPartyAttribute(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_third_party.txt") + + stringAttribute := NewString(protoAttribute) + + assert.Equal(t, stringAttribute.Value(), "test-third-party-attribute-0") + assert.Equal(t, stringAttribute.Name(), "com.thirdparty.id") + + assert.Equal(t, stringAttribute.Sources()[0].Value(), "THIRD_PARTY") + assert.Equal(t, stringAttribute.Sources()[0].SubType(), "orgName") + + assert.Equal(t, stringAttribute.Verifiers()[0].Value(), "THIRD_PARTY") + assert.Equal(t, stringAttribute.Verifiers()[0].SubType(), "orgName") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/base_profile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/base_profile.go new file mode 100644 index 0000000..693441d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/base_profile.go @@ -0,0 +1,75 @@ +package digitalidentity + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +type baseProfile struct { + attributeSlice []*yotiprotoattr.Attribute +} + +// GetAttribute retrieve an attribute by name on the Yoti profile. Will return nil if attribute is not present. +func (p baseProfile) GetAttribute(attributeName string) *attribute.GenericAttribute { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + return attribute.NewGeneric(a) + } + } + return nil +} + +// GetAttributeByID retrieve an attribute by ID on the Yoti profile. Will return nil if attribute is not present. +func (p baseProfile) GetAttributeByID(attributeID string) *attribute.GenericAttribute { + for _, a := range p.attributeSlice { + if a.EphemeralId == attributeID { + return attribute.NewGeneric(a) + } + } + return nil +} + +// GetAttributes retrieve a list of attributes by name on the Yoti profile. Will return an empty list of attribute is not present. +func (p baseProfile) GetAttributes(attributeName string) []*attribute.GenericAttribute { + var attributes []*attribute.GenericAttribute + for _, a := range p.attributeSlice { + if a.Name == attributeName { + attributes = append(attributes, attribute.NewGeneric(a)) + } + } + return attributes +} + +// GetStringAttribute retrieves a string attribute by name. Will return nil if attribute is not present. +func (p baseProfile) GetStringAttribute(attributeName string) *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + return attribute.NewString(a) + } + } + return nil +} + +// GetImageAttribute retrieves an image attribute by name. Will return nil if attribute is not present. +func (p baseProfile) GetImageAttribute(attributeName string) *attribute.ImageAttribute { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + imageAttribute, err := attribute.NewImage(a) + + if err == nil { + return imageAttribute + } + } + } + return nil +} + +// GetJSONAttribute retrieves a JSON attribute by name. Will return nil if attribute is not present. +func (p baseProfile) GetJSONAttribute(attributeName string) (*attribute.JSONAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + return attribute.NewJSON(a) + } + } + return nil, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/policy_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/policy_builder.go new file mode 100644 index 0000000..4dec515 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/policy_builder.go @@ -0,0 +1,261 @@ +package digitalidentity + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" +) + +const ( + authTypeSelfieConst = 1 + authTypePinConst = 2 +) + +// PolicyBuilder constructs a json payload specifying the dynamic policy +// for a dynamic scenario +type PolicyBuilder struct { + wantedAttributes map[string]WantedAttribute + wantedAuthTypes map[int]bool + isWantedRememberMe bool + err error + identityProfileRequirements *json.RawMessage + advancedIdentityProfileRequirements *json.RawMessage +} + +// Policy represents a dynamic policy for a share +type Policy struct { + attributes []WantedAttribute + authTypes []int + rememberMeID bool + identityProfileRequirements *json.RawMessage + advancedIdentityProfileRequirements *json.RawMessage +} + +// WithWantedAttribute adds an attribute from WantedAttributeBuilder to the policy +func (b *PolicyBuilder) WithWantedAttribute(attribute WantedAttribute) *PolicyBuilder { + if b.wantedAttributes == nil { + b.wantedAttributes = make(map[string]WantedAttribute) + } + var key string + if attribute.derivation != "" { + key = attribute.derivation + } else { + key = attribute.name + } + b.wantedAttributes[key] = attribute + return b +} + +// WithWantedAttributeByName adds an attribute by its name. This is not the preferred +// way of adding an attribute - instead use the other methods below. +// Options allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithWantedAttributeByName(name string, options ...interface{}) *PolicyBuilder { + attributeBuilder := (&WantedAttributeBuilder{}).WithName(name) + + for _, option := range options { + switch value := option.(type) { + case SourceConstraint: + attributeBuilder.WithConstraint(&value) + case constraintInterface: + attributeBuilder.WithConstraint(value) + default: + panic(fmt.Sprintf("not a valid option type, %v", value)) + } + } + + attribute, err := attributeBuilder.Build() + if err != nil { + b.err = yotierror.MultiError{This: err, Next: b.err} + } + b.WithWantedAttribute(attribute) + return b +} + +// WithFamilyName adds the family name attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithFamilyName(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrFamilyName, options...) +} + +// WithGivenNames adds the given names attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithGivenNames(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrGivenNames, options...) +} + +// WithFullName adds the full name attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithFullName(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrFullName, options...) +} + +// WithDateOfBirth adds the date of birth attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithDateOfBirth(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrDateOfBirth, options...) +} + +// WithGender adds the gender attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithGender(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrGender, options...) +} + +// WithPostalAddress adds the postal address attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithPostalAddress(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrAddress, options...) +} + +// WithStructuredPostalAddress adds the structured postal address attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithStructuredPostalAddress(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrStructuredPostalAddress, options...) +} + +// WithNationality adds the nationality attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithNationality(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrNationality, options...) +} + +// WithPhoneNumber adds the phone number attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithPhoneNumber(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrMobileNumber, options...) +} + +// WithSelfie adds the selfie attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithSelfie(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrSelfie, options...) +} + +// WithEmail adds the email address attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithEmail(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrEmailAddress, options...) +} + +// WithDocumentImages adds the document images attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithDocumentImages(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrDocumentImages, options...) +} + +// WithDocumentDetails adds the document details attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithDocumentDetails(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrDocumentDetails, options...) +} + +// WithAgeDerivedAttribute is a helper method for setting age based derivations +// Prefer to use WithAgeOver and WithAgeUnder instead of using this directly. +// "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithAgeDerivedAttribute(derivation string, options ...interface{}) *PolicyBuilder { + var attributeBuilder WantedAttributeBuilder + attributeBuilder. + WithName(consts.AttrDateOfBirth). + WithDerivation(derivation) + + for _, option := range options { + switch value := option.(type) { + case SourceConstraint: + attributeBuilder.WithConstraint(&value) + case constraintInterface: + attributeBuilder.WithConstraint(value) + default: + panic(fmt.Sprintf("not a valid option type, %v", value)) + } + } + + attr, err := attributeBuilder.Build() + if err != nil { + b.err = yotierror.MultiError{This: err, Next: b.err} + } + return b.WithWantedAttribute(attr) +} + +// WithAgeOver sets this dynamic policy as requesting whether the user is older than a certain age. +// "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithAgeOver(age int, options ...interface{}) *PolicyBuilder { + return b.WithAgeDerivedAttribute(fmt.Sprintf(consts.AttrAgeOver, age), options...) +} + +// WithAgeUnder sets this dynamic policy as requesting whether the user is younger +// than a certain age, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithAgeUnder(age int, options ...interface{}) *PolicyBuilder { + return b.WithAgeDerivedAttribute(fmt.Sprintf(consts.AttrAgeUnder, age), options...) +} + +// WithWantedRememberMe sets the Policy as requiring a "Remember Me ID" +func (b *PolicyBuilder) WithWantedRememberMe() *PolicyBuilder { + b.isWantedRememberMe = true + return b +} + +// WithWantedAuthType sets this dynamic policy as requiring a specific authentication type +func (b *PolicyBuilder) WithWantedAuthType(wantedAuthType int) *PolicyBuilder { + if b.wantedAuthTypes == nil { + b.wantedAuthTypes = make(map[int]bool) + } + b.wantedAuthTypes[wantedAuthType] = true + return b +} + +// WithSelfieAuth sets this dynamic policy as requiring Selfie-based authentication +func (b *PolicyBuilder) WithSelfieAuth() *PolicyBuilder { + return b.WithWantedAuthType(authTypeSelfieConst) +} + +// WithPinAuth sets this dynamic policy as requiring PIN authentication +func (b *PolicyBuilder) WithPinAuth() *PolicyBuilder { + return b.WithWantedAuthType(authTypePinConst) +} + +// WithIdentityProfileRequirements adds Identity Profile Requirements to the policy. Must be valid JSON. +func (b *PolicyBuilder) WithIdentityProfileRequirements(identityProfile json.RawMessage) *PolicyBuilder { + b.identityProfileRequirements = &identityProfile + return b +} + +// WithAdvancedIdentityProfileRequirements adds Advanced Identity Profile Requirements to the policy. Must be valid JSON. +func (b *PolicyBuilder) WithAdvancedIdentityProfileRequirements(advancedIdentityProfile json.RawMessage) *PolicyBuilder { + b.advancedIdentityProfileRequirements = &advancedIdentityProfile + return b +} + +// Build constructs a dynamic policy object +func (b *PolicyBuilder) Build() (Policy, error) { + return Policy{ + attributes: b.attributesAsList(), + authTypes: b.authTypesAsList(), + rememberMeID: b.isWantedRememberMe, + identityProfileRequirements: b.identityProfileRequirements, + advancedIdentityProfileRequirements: b.advancedIdentityProfileRequirements, + }, b.err +} + +func (b *PolicyBuilder) attributesAsList() []WantedAttribute { + attributeList := make([]WantedAttribute, 0) + for _, attr := range b.wantedAttributes { + attributeList = append(attributeList, attr) + } + return attributeList +} + +func (b *PolicyBuilder) authTypesAsList() []int { + authTypeList := make([]int, 0) + for auth, boolValue := range b.wantedAuthTypes { + if boolValue { + authTypeList = append(authTypeList, auth) + } + } + return authTypeList +} + +// MarshalJSON returns the JSON encoding +func (policy *Policy) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Wanted []WantedAttribute `json:"wanted"` + WantedAuthTypes []int `json:"wanted_auth_types"` + WantedRememberMe bool `json:"wanted_remember_me"` + IdentityProfileRequirements *json.RawMessage `json:"identity_profile_requirements,omitempty"` + AdvancedIdentityProfileRequirements *json.RawMessage `json:"advanced_identity_profile_requirements,omitempty"` + }{ + Wanted: policy.attributes, + WantedAuthTypes: policy.authTypes, + WantedRememberMe: policy.rememberMeID, + IdentityProfileRequirements: policy.identityProfileRequirements, + AdvancedIdentityProfileRequirements: policy.advancedIdentityProfileRequirements, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/policy_builder_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/policy_builder_test.go new file mode 100644 index 0000000..a7ae6e1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/policy_builder_test.go @@ -0,0 +1,508 @@ +package digitalidentity + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" + "gotest.tools/v3/assert" +) + +func ExamplePolicyBuilder_WithFamilyName() { + policy, err := (&PolicyBuilder{}).WithFamilyName().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"family_name","accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithDocumentDetails() { + policy, err := (&PolicyBuilder{}).WithDocumentDetails().Build() + if err != nil { + return + } + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"document_details","accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithDocumentImages() { + policy, err := (&PolicyBuilder{}).WithDocumentImages().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"document_images","accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithSelfie() { + policy, err := (&PolicyBuilder{}).WithSelfie().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"selfie","accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithAgeOver() { + constraint, err := (&SourceConstraintBuilder{}).WithDrivingLicence("").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + policy, err := (&PolicyBuilder{}).WithAgeOver(18, constraint).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"date_of_birth","derivation":"age_over:18","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[{"name":"DRIVING_LICENCE","sub_type":""}],"soft_preference":false}}],"accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithSelfieAuth() { + policy, err := (&PolicyBuilder{}).WithSelfieAuth().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[],"wanted_auth_types":[1],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithWantedRememberMe() { + policy, err := (&PolicyBuilder{}).WithWantedRememberMe().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[],"wanted_auth_types":[],"wanted_remember_me":true} +} + +func ExamplePolicyBuilder_WithFullName() { + constraint, err := (&SourceConstraintBuilder{}).WithPassport("").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + policy, err := (&PolicyBuilder{}).WithFullName(&constraint).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + marshalledJSON, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"wanted":[{"name":"full_name","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[{"name":"PASSPORT","sub_type":""}],"soft_preference":false}}],"accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder() { + policy, err := (&PolicyBuilder{}).WithFullName(). + WithPinAuth().WithWantedRememberMe().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"full_name","accept_self_asserted":false}],"wanted_auth_types":[2],"wanted_remember_me":true} +} + +func ExamplePolicyBuilder_WithAgeUnder() { + policy, err := (&PolicyBuilder{}).WithAgeUnder(18).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"date_of_birth","derivation":"age_under:18","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithGivenNames() { + policy, err := (&PolicyBuilder{}).WithGivenNames().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"given_names","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithDateOfBirth() { + policy, err := (&PolicyBuilder{}).WithDateOfBirth().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"date_of_birth","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithGender() { + policy, err := (&PolicyBuilder{}).WithGender().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"gender","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithPostalAddress() { + policy, err := (&PolicyBuilder{}).WithPostalAddress().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"postal_address","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithStructuredPostalAddress() { + policy, err := (&PolicyBuilder{}).WithStructuredPostalAddress().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"structured_postal_address","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithNationality() { + policy, err := (&PolicyBuilder{}).WithNationality().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"nationality","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithPhoneNumber() { + policy, err := (&PolicyBuilder{}).WithPhoneNumber().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"phone_number","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func TestDigitalIdentityBuilder_WithWantedAttributeByName_WithSourceConstraint(t *testing.T) { + attributeName := "attributeName" + builder := &PolicyBuilder{} + sourceConstraint, err := (&SourceConstraintBuilder{}).Build() + assert.NilError(t, err) + + builder.WithWantedAttributeByName( + attributeName, + sourceConstraint, + ) + + policy, err := builder.Build() + assert.NilError(t, err) + assert.Equal(t, len(policy.attributes), 1) + assert.Equal(t, policy.attributes[0].name, attributeName) + assert.Equal(t, len(policy.attributes[0].constraints), 1) +} + +func TestDigitalIdentityBuilder_WithWantedAttributeByName_InvalidOptionsShouldPanic(t *testing.T) { + attributeName := "attributeName" + builder := &PolicyBuilder{} + invalidOption := "invalidOption" + + defer func() { + r := recover().(string) + assert.Check(t, strings.Contains(r, "not a valid option type")) + }() + + builder.WithWantedAttributeByName( + attributeName, + invalidOption, + ) + + t.Error("Expected Panic") + +} + +func TestDigitalIdentityBuilder_WithWantedAttributeByName_ShouldPropagateErrors(t *testing.T) { + builder := &PolicyBuilder{} + + builder.WithWantedAttributeByName("") + builder.WithWantedAttributeByName("") + + _, err := builder.Build() + + assert.Error(t, err, "wanted attribute names must not be empty, wanted attribute names must not be empty") + assert.Error(t, err.(yotierror.MultiError).Unwrap(), "wanted attribute names must not be empty") +} + +func TestDigitalIdentityBuilder_WithAgeDerivedAttribute_WithSourceConstraint(t *testing.T) { + builder := &PolicyBuilder{} + sourceConstraint, err := (&SourceConstraintBuilder{}).Build() + assert.NilError(t, err) + + builder.WithAgeDerivedAttribute( + fmt.Sprintf(consts.AttrAgeOver, 18), + sourceConstraint, + ) + + policy, err := builder.Build() + assert.NilError(t, err) + assert.Equal(t, len(policy.attributes), 1) + assert.Equal(t, policy.attributes[0].name, consts.AttrDateOfBirth) + assert.Equal(t, len(policy.attributes[0].constraints), 1) +} + +func TestDigitalIdentityBuilder_WithAgeDerivedAttribute_WithConstraintInterface(t *testing.T) { + builder := &PolicyBuilder{} + var constraint constraintInterface + sourceConstraint, err := (&SourceConstraintBuilder{}).Build() + constraint = &sourceConstraint + assert.NilError(t, err) + + builder.WithAgeDerivedAttribute( + fmt.Sprintf(consts.AttrAgeOver, 18), + constraint, + ) + + policy, err := builder.Build() + assert.NilError(t, err) + assert.Equal(t, len(policy.attributes), 1) + assert.Equal(t, policy.attributes[0].name, consts.AttrDateOfBirth) + assert.Equal(t, len(policy.attributes[0].constraints), 1) +} + +func TestDigitalIdentityBuilder_WithAgeDerivedAttribute_InvalidOptionsShouldPanic(t *testing.T) { + builder := &PolicyBuilder{} + invalidOption := "invalidOption" + + defer func() { + r := recover().(string) + assert.Check(t, strings.Contains(r, "not a valid option type")) + }() + + builder.WithAgeDerivedAttribute( + fmt.Sprintf(consts.AttrAgeOver, 18), + invalidOption, + ) + + t.Error("Expected Panic") + +} + +func TestDigitalIdentityBuilder_WithIdentityProfileRequirements_ShouldFailForInvalidJSON(t *testing.T) { + identityProfile := []byte(`{ + "trust_framework": UK_TFIDA", + , + }`) + + policy, err := (&PolicyBuilder{}).WithIdentityProfileRequirements(identityProfile).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + _, err = policy.MarshalJSON() + if err == nil { + t.Error("expected an error") + } + var marshallerErr *json.MarshalerError + if !errors.As(err, &marshallerErr) { + t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err)) + } +} + +func ExamplePolicyBuilder_WithAdvancedIdentityProfileRequirements() { + advancedIdentityProfile := []byte(`{ + "profiles": [ + { + "trust_framework": "UK_TFIDA", + "schemes": [ + { + "label": "LB912", + "type": "RTW" + }, + { + "label": "LB777", + "type": "DBS", + "objective": "BASIC" + } + ] + }, + { + "trust_framework": "YOTI_GLOBAL", + "schemes": [ + { + "label": "LB321", + "type": "IDENTITY", + "objective": "AL_L1", + "config": {} + } + ] + } + ] + }`) + + policy, err := (&PolicyBuilder{}).WithAdvancedIdentityProfileRequirements(advancedIdentityProfile).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[],"wanted_auth_types":[],"wanted_remember_me":false,"advanced_identity_profile_requirements":{"profiles":[{"trust_framework":"UK_TFIDA","schemes":[{"label":"LB912","type":"RTW"},{"label":"LB777","type":"DBS","objective":"BASIC"}]},{"trust_framework":"YOTI_GLOBAL","schemes":[{"label":"LB321","type":"IDENTITY","objective":"AL_L1","config":{}}]}]}} +} + +func TestPolicyBuilder_WithAdvancedIdentityProfileRequirements_ShouldFailForInvalidJSON(t *testing.T) { + advancedIdentityProfile := []byte(`{ + "trust_framework": UK_TFIDA", + , + }`) + + policy, err := (&PolicyBuilder{}).WithAdvancedIdentityProfileRequirements(advancedIdentityProfile).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + _, err = policy.MarshalJSON() + if err == nil { + t.Error("expected an error") + } + var marshallerErr *json.MarshalerError + if !errors.As(err, &marshallerErr) { + t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err)) + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/qr_code.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/qr_code.go new file mode 100644 index 0000000..bff20b6 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/qr_code.go @@ -0,0 +1,6 @@ +package digitalidentity + +type QrCode struct { + Id string `json:"id"` + Uri string `json:"uri"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/receipt.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/receipt.go new file mode 100644 index 0000000..3c39c9b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/receipt.go @@ -0,0 +1,32 @@ +package digitalidentity + +type Content struct { + Profile []byte `json:"profile"` + ExtraData []byte `json:"extraData"` +} + +type RequirementsNotMetDetail struct { + Details string `json:"details"` + AuditId string `json:"audit_id"` + FailureType string `json:"failure_type"` + DocumentType string `json:"document_type"` + DocumentCountryIsoCode string `json:"document_country_iso_code"` +} + +type ErrorReason struct { + RequirementsNotMetDetails []RequirementsNotMetDetail `json:"requirements_not_met_details"` +} + +type ReceiptResponse struct { + ID string `json:"id"` + SessionID string `json:"sessionId"` + Timestamp string `json:"timestamp"` + RememberMeID string `json:"rememberMeId,omitempty"` + ParentRememberMeID string `json:"parentRememberMeId,omitempty"` + Content *Content `json:"content,omitempty"` + OtherPartyContent *Content `json:"otherPartyContent,omitempty"` + WrappedItemKeyId string `json:"wrappedItemKeyId"` + WrappedKey []byte `json:"wrappedKey"` + Error string `json:"error"` + ErrorReason ErrorReason `json:"errorReason"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/receipt_item_key.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/receipt_item_key.go new file mode 100644 index 0000000..7e805d8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/receipt_item_key.go @@ -0,0 +1,7 @@ +package digitalidentity + +type ReceiptItemKeyResponse struct { + ID string `json:"id"` + Iv []byte `json:"iv"` + Value []byte `json:"value"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/client.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/client.go new file mode 100644 index 0000000..74c289e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/client.go @@ -0,0 +1,10 @@ +package requests + +import ( + "net/http" +) + +// HttpClient is a mockable HTTP Client Interface +type HttpClient interface { + Do(*http.Request) (*http.Response, error) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/request.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/request.go new file mode 100644 index 0000000..e5bbeab --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/request.go @@ -0,0 +1,40 @@ +package requests + +import ( + "net/http" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/digitalidentity/yotierror" +) + +// Execute makes a request to the specified endpoint, with an optional payload +func Execute(httpClient HttpClient, request *http.Request) (response *http.Response, err error) { + + if response, err = doRequest(request, httpClient); err != nil { + + return + } + + statusCodeIsFailure := response.StatusCode >= 300 || response.StatusCode < 200 + + if statusCodeIsFailure { + return response, yotierror.NewResponseError(response) + } + + return response, nil +} + +func doRequest(request *http.Request, httpClient HttpClient) (*http.Response, error) { + httpClient = ensureHttpClientTimeout(httpClient) + return httpClient.Do(request) +} + +func ensureHttpClientTimeout(httpClient HttpClient) HttpClient { + if httpClient == nil { + httpClient = &http.Client{ + Timeout: time.Second * 10, + } + } + + return httpClient +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/request_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/request_test.go new file mode 100644 index 0000000..420fa6b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/request_test.go @@ -0,0 +1,71 @@ +package requests + +import ( + "net/http" + "testing" + "time" + + "gotest.tools/v3/assert" +) + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func TestExecute_Success(t *testing.T) { + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + }, nil + }, + } + + request := &http.Request{ + Method: http.MethodGet, + } + + response, err := Execute(client, request) + + assert.NilError(t, err) + assert.Equal(t, response.StatusCode, 200) +} + +func TestExecute_Failure(t *testing.T) { + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 400, + }, nil + }, + } + + request := &http.Request{ + Method: http.MethodGet, + } + + _, err := Execute(client, request) + assert.ErrorContains(t, err, "unknown HTTP error") +} + +func TestEnsureHttpClientTimeout_NilHTTPClientShouldUse10sTimeout(t *testing.T) { + result := ensureHttpClientTimeout(nil).(*http.Client) + + assert.Equal(t, 10*time.Second, result.Timeout) +} + +func TestEnsureHttpClientTimeout(t *testing.T) { + httpClient := &http.Client{ + Timeout: time.Minute * 12, + } + result := ensureHttpClientTimeout(httpClient).(*http.Client) + + assert.Equal(t, 12*time.Minute, result.Timeout) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/signed_message.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/signed_message.go new file mode 100644 index 0000000..02e2116 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/signed_message.go @@ -0,0 +1,233 @@ +package requests + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/consts" +) + +// MergeHeaders merges two or more header prototypes together from left to right +func MergeHeaders(headers ...map[string][]string) map[string][]string { + if len(headers) == 0 { + return make(map[string][]string) + } + out := headers[0] + for _, element := range headers[1:] { + for k, v := range element { + out[k] = v + } + } + return out +} + +// JSONHeaders is a header prototype for JSON based requests +func JSONHeaders() map[string][]string { + return map[string][]string{ + "Content-Type": {"application/json"}, + "Accept": {"application/json"}, + } +} + +// AuthHeader is a header prototype including the App/SDK ID +func AuthHeader(clientSdkId string) map[string][]string { + return map[string][]string{ + "X-Yoti-Auth-Id": {clientSdkId}, + } +} + +// AuthKeyHeader is a header prototype including an encoded RSA PublicKey +func AuthKeyHeader(key *rsa.PublicKey) map[string][]string { + return map[string][]string{ + "X-Yoti-Auth-Key": { + base64.StdEncoding.EncodeToString( + func(a []byte, _ error) []byte { + return a + }(x509.MarshalPKIXPublicKey(key)), + ), + }, + } +} + +// SignedRequest is a builder for constructing a http.Request with Yoti signing +type SignedRequest struct { + Key *rsa.PrivateKey + HTTPMethod string + BaseURL string + Endpoint string + Headers map[string][]string + Params map[string]string + Body []byte + Error error +} + +func (msg *SignedRequest) signDigest(digest []byte) (string, error) { + hash := sha256.Sum256(digest) + signed, err := rsa.SignPKCS1v15(rand.Reader, msg.Key, crypto.SHA256, hash[:]) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(signed), nil +} + +func getTimestamp() string { + return strconv.FormatInt(time.Now().Unix()*1000, 10) +} + +func getNonce() (string, error) { + nonce := make([]byte, 16) + _, err := rand.Read(nonce) + return fmt.Sprintf("%X-%X-%X-%X-%X", nonce[0:4], nonce[4:6], nonce[6:8], nonce[8:10], nonce[10:]), err +} + +// WithPemFile loads the private key from a PEM file reader +func (msg SignedRequest) WithPemFile(in []byte) SignedRequest { + block, _ := pem.Decode(in) + if block == nil { + msg.Error = errors.New("input is not PEM-encoded") + return msg + } + if block.Type != "RSA PRIVATE KEY" { + msg.Error = errors.New("input is not an RSA Private Key") + return msg + } + + msg.Key, msg.Error = x509.ParsePKCS1PrivateKey(block.Bytes) + return msg +} + +func (msg *SignedRequest) addParametersToEndpoint() (string, error) { + if msg.Params == nil { + msg.Params = make(map[string]string) + } + // Add Timestamp/Nonce + if _, ok := msg.Params["nonce"]; !ok { + nonce, err := getNonce() + if err != nil { + return "", err + } + msg.Params["nonce"] = nonce + } + if _, ok := msg.Params["timestamp"]; !ok { + msg.Params["timestamp"] = getTimestamp() + } + + endpoint := msg.Endpoint + if !strings.Contains(endpoint, "?") { + endpoint = endpoint + "?" + } else { + endpoint = endpoint + "&" + } + + var firstParam = true + for param, value := range msg.Params { + var formatString = "%s&%s=%s" + if firstParam { + formatString = "%s%s=%s" + } + endpoint = fmt.Sprintf(formatString, endpoint, param, value) + firstParam = false + } + + return endpoint, nil +} + +func (msg *SignedRequest) generateDigest(endpoint string) (digest string) { + // Generate the message digest + if msg.Body != nil { + digest = fmt.Sprintf( + "%s&%s&%s", + msg.HTTPMethod, + endpoint, + base64.StdEncoding.EncodeToString(msg.Body), + ) + } else { + digest = fmt.Sprintf("%s&%s", + msg.HTTPMethod, + endpoint, + ) + } + return +} + +func (msg *SignedRequest) checkMandatories() error { + if msg.Error != nil { + return msg.Error + } + if msg.Key == nil { + return fmt.Errorf("missing private key") + } + if msg.HTTPMethod == "" { + return fmt.Errorf("missing HTTPMethod") + } + if msg.BaseURL == "" { + return fmt.Errorf("missing BaseURL") + } + if msg.Endpoint == "" { + return fmt.Errorf("missing Endpoint") + } + return nil +} + +// Request builds a http.Request with signature headers +func (msg SignedRequest) Request() (request *http.Request, err error) { + err = msg.checkMandatories() + if err != nil { + return + } + + endpoint, err := msg.addParametersToEndpoint() + if err != nil { + return + } + + signedDigest, err := msg.signDigest([]byte(msg.generateDigest(endpoint))) + if err != nil { + return + } + + // Construct the HTTP Request + request, err = http.NewRequest( + msg.HTTPMethod, + msg.BaseURL+endpoint, + bytes.NewReader(msg.Body), + ) + if err != nil { + return + } + + request.Header.Add("X-Yoti-Auth-Digest", signedDigest) + request.Header.Add("X-Yoti-SDK", consts.SDKIdentifier) + request.Header.Add("X-Yoti-SDK-Version", consts.SDKVersionIdentifier) + + for key, values := range msg.Headers { + for _, value := range values { + request.Header.Add(key, value) + } + } + + return request, err +} + +func Base64ToBase64URL(base64Str string) string { + decoded, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + return "" + } + + base64URL := base64.URLEncoding.EncodeToString(decoded) + + return base64URL +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/signed_message_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/signed_message_test.go new file mode 100644 index 0000000..1e23205 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/requests/signed_message_test.go @@ -0,0 +1,169 @@ +package requests + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "regexp" + "testing" + + "gotest.tools/v3/assert" +) + +const exampleKey = "MIICXgIBAAKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQABAoGBAIJL7GbSvjZUVVU1E6TZd0+9lhqmGf/S2o5309bxSfQ/oxxSyrHU9nMNTqcjCZXuJCTKS7hOKmXY5mbOYvvZ0xA7DXfOc+A4LGXQl0r3ZMzhHZTPKboUSh16E4WI4pr98KagFdkeB/0KBURM3x5d/6dSKip8ZpEyqVpuc9d1xtvhAkEAxabfsqfb4fgBsrhZ/qt133yB0FBHs1alRxvUXZWbVPTOegKi5KBdPptf2QfCy8WK3An/lg8cFQG78PyNll/P0QJBANtJBUHTuRDCoYLhqZLdSTQ52qOWRNutZ2fho9ZcLquokB4SFFeC2I4T+s3oSJ8SNh9vW1nNeXW6Zipx+zz8O58CQQCjV9qNGf40zDITEhmFxwt967aYgpAO3O9wScaCpM4fMsWkvaMDEKiewec/RBOvNY0hdb3ctJX/olRAv2b/vCTRAkAuLmCnDlnJR9QP5kp6HZRPJWgAT6NMyGYgoIqKmHtTt3oyewhBrdLBiT+moaa5qXIwiJkqfnV377uYcMzCeTRtAkEAwHdhM3v01GprmHqE2kvlKOXNq9CB1Z4j/vXSQxBYoSrFWLv5nW9e69ngX+n7qhvO3Gs9CBoy/oqOLatFZOuFEw==" + +var keyBytes, _ = base64.StdEncoding.DecodeString(exampleKey) +var privateKey, _ = x509.ParsePKCS1PrivateKey(keyBytes) + +func ExampleMergeHeaders() { + left := map[string][]string{"A": {"Value Of A"}} + right := map[string][]string{"B": {"Value Of B"}} + + merged := MergeHeaders(left, right) + fmt.Println(merged["A"]) + fmt.Println(merged["B"]) + // Output: + // [Value Of A] + // [Value Of B] +} + +func TestMergeHeaders_HandleNullCaseGracefully(t *testing.T) { + assert.Equal(t, len(MergeHeaders()), 0) +} + +func ExampleJSONHeaders() { + jsonHeaders, err := json.Marshal(JSONHeaders()) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(jsonHeaders)) + // Output: {"Accept":["application/json"],"Content-Type":["application/json"]} +} + +func ExampleAuthKeyHeader() { + headers, err := json.Marshal(AuthKeyHeader(&privateKey.PublicKey)) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(headers)) + // Output: {"X-Yoti-Auth-Key":["MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQAB"]} +} + +func TestRequestShouldBuildForValid(t *testing.T) { + random := rand.New(rand.NewSource(25)) + key, err := rsa.GenerateKey(random, 1024) + + assert.NilError(t, err) + httpMethod := "GET" + baseURL := "example.com" + endpoint := "/" + + request := SignedRequest{ + Key: key, + HTTPMethod: httpMethod, + BaseURL: baseURL, + Endpoint: endpoint, + } + signed, err := request.Request() + assert.NilError(t, err) + assert.Equal(t, httpMethod, signed.Method) + urlCheck, err := regexp.Match(baseURL+endpoint, []byte(signed.URL.String())) + assert.NilError(t, err) + assert.Check(t, urlCheck) + assert.Check(t, signed.Header.Get("X-Yoti-Auth-Digest") != "") + assert.Equal(t, signed.Header.Get("X-Yoti-SDK"), "Go") + assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.14.0") +} + +func TestRequestShouldAddHeaders(t *testing.T) { + random := rand.New(rand.NewSource(25)) + key, err := rsa.GenerateKey(random, 1024) + + assert.NilError(t, err) + httpMethod := "GET" + baseURL := "example.com" + endpoint := "/" + + request := SignedRequest{ + Key: key, + HTTPMethod: httpMethod, + BaseURL: baseURL, + Endpoint: endpoint, + Headers: JSONHeaders(), + } + signed, err := request.Request() + assert.NilError(t, err) + assert.Check(t, signed.Header["X-Yoti-Auth-Digest"][0] != "") + assert.Equal(t, signed.Header["Accept"][0], "application/json") +} + +func TestSignedRequest_checkMandatories_WhenErrorIsSetReturnIt(t *testing.T) { + msg := &SignedRequest{Error: fmt.Errorf("exampleError")} + assert.Error(t, msg.checkMandatories(), "exampleError") +} + +func TestSignedRequest_checkMandatories_WhenKeyMissing(t *testing.T) { + msg := &SignedRequest{} + assert.Error(t, msg.checkMandatories(), "missing private key") +} + +func TestSignedRequest_checkMandatories_WhenHTTPMethodMissing(t *testing.T) { + msg := &SignedRequest{Key: privateKey} + assert.Error(t, msg.checkMandatories(), "missing HTTPMethod") +} + +func TestSignedRequest_checkMandatories_WhenBaseURLMissing(t *testing.T) { + msg := &SignedRequest{ + Key: privateKey, + HTTPMethod: http.MethodPost, + } + assert.Error(t, msg.checkMandatories(), "missing BaseURL") +} + +func TestSignedRequest_checkMandatories_WhenEndpointMissing(t *testing.T) { + msg := &SignedRequest{ + Key: privateKey, + HTTPMethod: http.MethodPost, + BaseURL: "example.com", + } + assert.Error(t, msg.checkMandatories(), "missing Endpoint") +} + +func ExampleSignedRequest_generateDigest() { + msg := &SignedRequest{ + HTTPMethod: http.MethodPost, + Body: []byte("simple message body"), + } + fmt.Println(msg.generateDigest("endpoint")) + // Output: POST&endpoint&c2ltcGxlIG1lc3NhZ2UgYm9keQ== + +} + +func ExampleSignedRequest_WithPemFile() { + msg := SignedRequest{}.WithPemFile([]byte(` +-----BEGIN RSA PRIVATE KEY----- +` + exampleKey + ` +-----END RSA PRIVATE KEY-----`)) + fmt.Println(AuthKeyHeader(&msg.Key.PublicKey)) + // Output: map[X-Yoti-Auth-Key:[MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQAB]] +} + +func TestSignedRequest_WithPemFile_NotPemEncodedShouldError(t *testing.T) { + msg := SignedRequest{}.WithPemFile([]byte("not pem encoded")) + assert.ErrorContains(t, msg.Error, "not PEM-encoded") +} + +func TestSignedRequest_WithPemFile_NotRSAKeyShouldError(t *testing.T) { + msg := SignedRequest{}.WithPemFile([]byte(`-----BEGIN RSA PUBLIC KEY----- +` + exampleKey + ` +-----END RSA PUBLIC KEY-----`)) + assert.ErrorContains(t, msg.Error, "not an RSA Private Key") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/service.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/service.go new file mode 100644 index 0000000..8a536ed --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/service.go @@ -0,0 +1,316 @@ +package digitalidentity + +import ( + "crypto/rsa" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/digitalidentity/requests" + "github.com/getyoti/yoti-go-sdk/v3/extra" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" +) + +const identitySessionCreationEndpoint = "/v2/sessions" +const identitySessionRetrieval = "/v2/sessions/%s" +const identitySessionQrCodeCreation = "/v2/sessions/%s/qr-codes" +const identitySessionQrCodeRetrieval = "/v2/qr-codes/%s" +const identitySessionReceiptRetrieval = "/v2/receipts/%s" +const identitySessionReceiptKeyRetrieval = "/v2/wrapped-item-keys/%s" +const errorFailedToGetSignedRequest = "failed to get signed request: %v" +const errorFailedToExecuteRequest = "failed to execute request: %v" +const errorFailedToReadBody = "failed to read response body: %v" + +// CreateShareSession creates session using the supplied session specification +func CreateShareSession(httpClient requests.HttpClient, shareSessionRequest *ShareSessionRequest, clientSdkId, apiUrl string, key *rsa.PrivateKey) (*ShareSession, error) { + endpoint := identitySessionCreationEndpoint + + payload, err := shareSessionRequest.MarshalJSON() + if err != nil { + return nil, err + } + + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodPost, + BaseURL: apiUrl, + Endpoint: endpoint, + Headers: requests.AuthHeader(clientSdkId), + Body: payload, + Params: map[string]string{"sdkID": clientSdkId}, + }.Request() + if err != nil { + return nil, fmt.Errorf(errorFailedToGetSignedRequest, err) + } + + response, err := requests.Execute(httpClient, request) + if err != nil { + return nil, fmt.Errorf(errorFailedToExecuteRequest, err) + } + + defer response.Body.Close() + shareSession := &ShareSession{} + + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf(errorFailedToReadBody, err) + } + err = json.Unmarshal(responseBytes, shareSession) + return shareSession, err +} + +// GetShareSession get session info using the supplied sessionID parameter +func GetShareSession(httpClient requests.HttpClient, sessionID string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (*ShareSession, error) { + endpoint := fmt.Sprintf(identitySessionRetrieval, sessionID) + + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodGet, + BaseURL: apiUrl, + Endpoint: endpoint, + Headers: requests.AuthHeader(clientSdkId), + Params: map[string]string{"sdkID": clientSdkId}, + }.Request() + if err != nil { + return nil, fmt.Errorf(errorFailedToGetSignedRequest, err) + } + + response, err := requests.Execute(httpClient, request) + + if err != nil { + return nil, fmt.Errorf(errorFailedToExecuteRequest, err) + } + defer response.Body.Close() + shareSession := &ShareSession{} + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf(errorFailedToReadBody, err) + } + err = json.Unmarshal(responseBytes, shareSession) + return shareSession, err +} + +// CreateShareQrCode generates a sharing qr code using the supplied sessionID parameter +func CreateShareQrCode(httpClient requests.HttpClient, sessionID string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (*QrCode, error) { + endpoint := fmt.Sprintf(identitySessionQrCodeCreation, sessionID) + + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodPost, + BaseURL: apiUrl, + Endpoint: endpoint, + Headers: requests.AuthHeader(clientSdkId), + Body: nil, + Params: map[string]string{"sdkID": clientSdkId}, + }.Request() + if err != nil { + return nil, fmt.Errorf(errorFailedToGetSignedRequest, err) + } + + response, err := requests.Execute(httpClient, request) + if err != nil { + return nil, fmt.Errorf(errorFailedToExecuteRequest, err) + } + + defer response.Body.Close() + qrCode := &QrCode{} + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf(errorFailedToReadBody, err) + } + err = json.Unmarshal(responseBytes, qrCode) + return qrCode, err +} + +// GetShareSessionQrCode is used to fetch the qr code by id. +func GetShareSessionQrCode(httpClient requests.HttpClient, qrCodeId string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (fetchedQrCode ShareSessionQrCode, err error) { + endpoint := fmt.Sprintf(identitySessionQrCodeRetrieval, qrCodeId) + headers := requests.AuthHeader(clientSdkId) + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodGet, + BaseURL: apiUrl, + Endpoint: endpoint, + Headers: headers, + }.Request() + if err != nil { + return fetchedQrCode, fmt.Errorf(errorFailedToGetSignedRequest, err) + } + + response, err := requests.Execute(httpClient, request) + if err != nil { + return fetchedQrCode, fmt.Errorf(errorFailedToExecuteRequest, err) + } + defer response.Body.Close() + + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return fetchedQrCode, fmt.Errorf(errorFailedToReadBody, err) + } + + err = json.Unmarshal(responseBytes, &fetchedQrCode) + + return fetchedQrCode, err +} + +// GetReceipt fetches receipt info using a receipt id. +func getReceipt(httpClient requests.HttpClient, receiptId string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (receipt ReceiptResponse, err error) { + receiptUrl := requests.Base64ToBase64URL(receiptId) + endpoint := fmt.Sprintf(identitySessionReceiptRetrieval, receiptUrl) + + headers := requests.AuthHeader(clientSdkId) + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodGet, + BaseURL: apiUrl, + Endpoint: endpoint, + Headers: headers, + }.Request() + if err != nil { + return receipt, fmt.Errorf(errorFailedToGetSignedRequest, err) + } + + response, err := requests.Execute(httpClient, request) + if err != nil { + return receipt, fmt.Errorf(errorFailedToExecuteRequest, err) + } + defer response.Body.Close() + + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return receipt, fmt.Errorf(errorFailedToReadBody, err) + } + + err = json.Unmarshal(responseBytes, &receipt) + + return receipt, err +} + +// GetReceiptItemKey retrieves the receipt item key for a receipt item key id. +func getReceiptItemKey(httpClient requests.HttpClient, receiptItemKeyId string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (receiptItemKey ReceiptItemKeyResponse, err error) { + endpoint := fmt.Sprintf(identitySessionReceiptKeyRetrieval, receiptItemKeyId) + headers := requests.AuthHeader(clientSdkId) + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodGet, + BaseURL: apiUrl, + Endpoint: endpoint, + Headers: headers, + }.Request() + if err != nil { + return receiptItemKey, fmt.Errorf(errorFailedToGetSignedRequest, err) + } + + response, err := requests.Execute(httpClient, request) + if err != nil { + return receiptItemKey, err + } + defer response.Body.Close() + + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return receiptItemKey, err + } + + err = json.Unmarshal(responseBytes, &receiptItemKey) + + return receiptItemKey, err +} + +func GetShareReceipt(httpClient requests.HttpClient, receiptId string, clientSdkId, apiUrl string, key *rsa.PrivateKey) (receipt SharedReceiptResponse, err error) { + receiptResponse, err := getReceipt(httpClient, receiptId, clientSdkId, apiUrl, key) + if err != nil { + return receipt, fmt.Errorf("failed to get receipt: %v", err) + } + + if receiptResponse.Error != "" { + return SharedReceiptResponse{ + ID: receiptResponse.ID, + SessionID: receiptResponse.SessionID, + Timestamp: receiptResponse.Timestamp, + Error: receiptResponse.Error, + ErrorReason: receiptResponse.ErrorReason, + }, nil + } + + itemKeyId := receiptResponse.WrappedItemKeyId + + encryptedItemKeyResponse, err := getReceiptItemKey(httpClient, itemKeyId, clientSdkId, apiUrl, key) + if err != nil { + return receipt, fmt.Errorf("failed to get receipt item key: %v", err) + } + + receiptContentKey, err := cryptoutil.UnwrapReceiptKey(receiptResponse.WrappedKey, encryptedItemKeyResponse.Value, encryptedItemKeyResponse.Iv, key) + if err != nil { + return receipt, fmt.Errorf("failed to unwrap receipt content key: %v", err) + } + + attrData, aextra, err := decryptReceiptContent(receiptResponse.Content, receiptContentKey) + if err != nil { + return receipt, fmt.Errorf("failed to decrypt receipt content: %v", err) + } + + applicationProfile := newApplicationProfile(attrData) + extraDataValue, err := extra.NewExtraData(aextra) + if err != nil { + return receipt, fmt.Errorf("failed to build application extra data: %v", err) + } + + uattrData, uextra, err := decryptReceiptContent(receiptResponse.OtherPartyContent, receiptContentKey) + if err != nil { + return receipt, fmt.Errorf("failed to decrypt receipt other party content: %v", err) + } + + userProfile := newUserProfile(uattrData) + userExtraDataValue, err := extra.NewExtraData(uextra) + if err != nil { + return receipt, fmt.Errorf("failed to build other party extra data: %v", err) + } + + return SharedReceiptResponse{ + ID: receiptResponse.ID, + SessionID: receiptResponse.SessionID, + RememberMeID: receiptResponse.RememberMeID, + ParentRememberMeID: receiptResponse.ParentRememberMeID, + Timestamp: receiptResponse.Timestamp, + UserContent: UserContent{ + UserProfile: userProfile, + ExtraData: userExtraDataValue, + }, + ApplicationContent: ApplicationContent{ + ApplicationProfile: applicationProfile, + ExtraData: extraDataValue, + }, + Error: receiptResponse.Error, + }, nil +} + +func decryptReceiptContent(content *Content, key []byte) (attrData *yotiprotoattr.AttributeList, aextra []byte, err error) { + + if content != nil { + if len(content.Profile) > 0 { + aattr, err := cryptoutil.DecryptReceiptContent(content.Profile, key) + if err != nil { + return nil, nil, fmt.Errorf("failed to decrypt content profile: %v", err) + } + + attrData = &yotiprotoattr.AttributeList{} + if err := proto.Unmarshal(aattr, attrData); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal attribute list: %v", err) + } + } + + if len(content.ExtraData) > 0 { + aextra, err = cryptoutil.DecryptReceiptContent(content.ExtraData, key) + if err != nil { + return nil, nil, fmt.Errorf("failed to decrypt receipt content extra data: %v", err) + } + } + + } + + return attrData, aextra, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/service_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/service_test.go new file mode 100644 index 0000000..a36df0d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/service_test.go @@ -0,0 +1,224 @@ +package digitalidentity + +import ( + "crypto/rsa" + "errors" + "fmt" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "io" + "net/http" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "gotest.tools/v3/assert" +) + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func ExampleCreateShareSession() { + key := test.GetValidKey("../test/test-key.pem") + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{"id":"0","status":"success","expiry": ""}`)), + }, nil + }, + } + + policy, err := (&PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + session, err := (&ShareSessionRequestBuilder{}).WithPolicy(policy).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + result, err := CreateShareSession(client, &session, "sdkId", "https://apiurl", key) + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Printf("Status code: %s", result.Status) + // Output: Status code: success +} + +func TestCreateShareURL_Unsuccessful_401(t *testing.T) { + _, err := createShareSessionWithErrorResponse(401, `{"id":"8f6a9dfe72128de20909af0d476769b6","status":401,"error":"INVALID_REQUEST_SIGNATURE","message":"Invalid request signature"}`) + + assert.ErrorContains(t, err, "INVALID_REQUEST_SIGNATURE") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func createShareSessionWithErrorResponse(statusCode int, responseBody string) (*ShareSession, error) { + key := test.GetValidKey("../test/test-key.pem") + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + }, + } + + policy, err := (&PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build() + if err != nil { + return nil, err + } + session, err := (&ShareSessionRequestBuilder{}).WithPolicy(policy).Build() + if err != nil { + return nil, err + } + + return CreateShareSession(client, &session, "sdkId", "https://apiurl", key) +} + +func TestGetShareSession(t *testing.T) { + key := test.GetValidKey("../test/test-key.pem") + mockSessionID := "SOME_SESSION_ID" + mockClientSdkId := "SOME_CLIENT_SDK_ID" + mockApiUrl := "https://example.com/api" + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{"id":"SOME_ID","status":"SOME_STATUS","expiry":"SOME_EXPIRY","created":"SOME_CREATED","updated":"SOME_UPDATED","qrCode":{"id":"SOME_QRCODE_ID"},"receipt":{"id":"SOME_RECEIPT_ID"}}`)), + }, nil + }, + } + + _, err := GetShareSession(client, mockSessionID, mockClientSdkId, mockApiUrl, key) + assert.NilError(t, err) + +} + +func TestCreateShareQrCode(t *testing.T) { + key := test.GetValidKey("../test/test-key.pem") + mockSessionID := "SOME_SESSION_ID" + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{}`)), + }, nil + }, + } + + _, err := CreateShareQrCode(client, mockSessionID, "sdkId", "https://apiurl", key) + assert.NilError(t, err) +} + +func TestGetQrCode(t *testing.T) { + key := test.GetValidKey("../test/test-key.pem") + mockQrId := "SOME_QR_CODE_ID" + mockClientSdkId := "SOME_CLIENT_SDK_ID" + mockApiUrl := "https://example.com/api" + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{}`)), + }, nil + }, + } + + _, err := GetShareSessionQrCode(client, mockQrId, mockClientSdkId, mockApiUrl, key) + assert.NilError(t, err) + +} + +// stub cryptoutil.UnwrapReceiptKey to control behaviour in tests +var unwrapReceiptKeyStub = func(wrappedKey, encryptedValue, iv []byte, key *rsa.PrivateKey) ([]byte, error) { + return []byte("receiptContentKey"), nil +} + +// stub decryptReceiptContent to simulate decryption +var decryptReceiptContentStub = func(content *Content, key []byte) (*yotiprotoattr.AttributeList, []byte, error) { + // Return dummy attribute list and extra data bytes + attrList := &yotiprotoattr.AttributeList{ + Attributes: []*yotiprotoattr.Attribute{{Name: "dummy", Value: []byte("value")}}, + } + return attrList, []byte("extraData"), nil +} + +func TestGetShareReceipt_GetReceiptError(t *testing.T) { + key := test.GetValidKey("../test/test-key.pem") + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return nil, errors.New("network error") + }, + } + + _, err := GetShareReceipt(client, "receiptId", "sdkId", "https://apiurl", key) + assert.ErrorContains(t, err, "failed to get receipt") +} + +func TestGetShareReceipt_GetReceiptItemKeyError(t *testing.T) { + key := test.GetValidKey("../test/test-key.pem") + + callCount := 0 + client := &mockHTTPClient{ + do: func(req *http.Request) (*http.Response, error) { + callCount++ + if callCount == 1 { + // return valid receipt JSON with WrappedItemKeyId + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{ + "WrappedItemKeyId": "itemKeyId", + "WrappedKey": "wrappedKeyData" + }`)), + }, nil + } + // simulate error on receipt item key request + return nil, errors.New("item key request error") + }, + } + + _, err := GetShareReceipt(client, "receiptId", "sdkId", "https://apiurl", key) + assert.ErrorContains(t, err, "failed to get receipt") +} + +func TestGetFailureReceipt(t *testing.T) { + key := test.GetValidKey("../test/test-key.pem") + mockQrId := "SOME_QR_CODE_ID" + mockClientSdkId := "SOME_CLIENT_SDK_ID" + mockApiUrl := "https://example.com/api" + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{"id":"tXTQK9E22lyzyIhVZM7pY1ctI7FHelLgvrVO35RO+vWKjJJJSJhu2ZLFBCce14Xy","sessionId":"ss.v2.ChZvRm1QTG5tT1FJMjZMYm9xeC1Fdlh3EgdsZDUuZ2Jy","timestamp":"2025-05-21T11:01:07Z","error":"MANDATORY_DOCUMENT_NOT_PROVIDED","errorReason":{"requirements_not_met_details":[{"details":"NOT_APPLICABLE_FOR_SCHEME","audit_id":"97001564-a18a-4afd-bf19-3ffacc88abbb","failure_type":"ID_DOCUMENT_COUNTRY","document_type":"PASSPORT","document_country_iso_code":"IRL"}]}}`)), + }, nil + }, + } + + r, err := GetShareReceipt(client, mockQrId, mockClientSdkId, mockApiUrl, key) + assert.Equal(t, len(r.ErrorReason.RequirementsNotMetDetails), 1) + assert.NilError(t, err) + +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_receipt.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_receipt.go new file mode 100644 index 0000000..399b6dc --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_receipt.go @@ -0,0 +1,25 @@ +package digitalidentity + +import "github.com/getyoti/yoti-go-sdk/v3/extra" + +type SharedReceiptResponse struct { + ID string + SessionID string + RememberMeID string + ParentRememberMeID string + Timestamp string + Error string + ErrorReason ErrorReason + UserContent UserContent + ApplicationContent ApplicationContent +} + +type ApplicationContent struct { + ApplicationProfile ApplicationProfile + ExtraData *extra.Data +} + +type UserContent struct { + UserProfile UserProfile + ExtraData *extra.Data +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_retrieve_qr.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_retrieve_qr.go new file mode 100644 index 0000000..fe737d9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_retrieve_qr.go @@ -0,0 +1,10 @@ +package digitalidentity + +type ShareSessionFetchedQrCode struct { + ID string `json:"id"` + Expiry string `json:"expiry"` + Policy string `json:"policy"` + Extensions []interface{} `json:"extensions"` + Session ShareSessionCreated `json:"session"` + RedirectURI string `json:"redirectUri"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session.go new file mode 100644 index 0000000..e27b99f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session.go @@ -0,0 +1,21 @@ +package digitalidentity + +// ShareSession contains information about the session. +type ShareSession struct { + Id string `json:"id"` + Status string `json:"status"` + Expiry string `json:"expiry"` + Created string `json:"created"` + Updated string `json:"updated"` + QrCode qrCode `json:"qrCode"` + Receipt *receipt `json:"receipt"` +} + +type qrCode struct { + Id string `json:"id"` +} + +// receipt containing the receipt id as a string. +type receipt struct { + Id string `json:"id"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_builder.go new file mode 100644 index 0000000..7f644a2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_builder.go @@ -0,0 +1,75 @@ +package digitalidentity + +import ( + "encoding/json" +) + +// ShareSessionRequestBuilder builds a session +type ShareSessionRequestBuilder struct { + shareSessionRequest ShareSessionRequest + err error +} + +// ShareSessionRequest represents a sharesession +type ShareSessionRequest struct { + policy Policy + extensions []interface{} + subject *json.RawMessage + shareSessionNotification *ShareSessionNotification + redirectUri string +} + +// WithPolicy attaches a Policy to the ShareSession +func (builder *ShareSessionRequestBuilder) WithPolicy(policy Policy) *ShareSessionRequestBuilder { + builder.shareSessionRequest.policy = policy + return builder +} + +// WithExtension adds an extension to the ShareSession +func (builder *ShareSessionRequestBuilder) WithExtension(extension interface{}) *ShareSessionRequestBuilder { + builder.shareSessionRequest.extensions = append(builder.shareSessionRequest.extensions, extension) + return builder +} + +// WithNotification sets the callback URL +func (builder *ShareSessionRequestBuilder) WithNotification(notification *ShareSessionNotification) *ShareSessionRequestBuilder { + builder.shareSessionRequest.shareSessionNotification = notification + return builder +} + +// WithRedirectUri sets redirectUri to the ShareSession +func (builder *ShareSessionRequestBuilder) WithRedirectUri(redirectUri string) *ShareSessionRequestBuilder { + builder.shareSessionRequest.redirectUri = redirectUri + return builder +} + +// WithSubject adds a subject to the ShareSession. Must be valid JSON. +func (builder *ShareSessionRequestBuilder) WithSubject(subject json.RawMessage) *ShareSessionRequestBuilder { + builder.shareSessionRequest.subject = &subject + return builder +} + +// Build constructs the ShareSession +func (builder *ShareSessionRequestBuilder) Build() (ShareSessionRequest, error) { + if builder.shareSessionRequest.extensions == nil { + builder.shareSessionRequest.extensions = make([]interface{}, 0) + } + return builder.shareSessionRequest, builder.err +} + +// MarshalJSON returns the JSON encoding +func (shareSesssion ShareSessionRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Policy Policy `json:"policy"` + Extensions []interface{} `json:"extensions"` + RedirectUri string `json:"redirectUri"` + Subject *json.RawMessage `json:"subject,omitempty"` + Notification *ShareSessionNotification `json:"notification,omitempty"` + }{ + Policy: shareSesssion.policy, + Extensions: shareSesssion.extensions, + RedirectUri: shareSesssion.redirectUri, + Subject: shareSesssion.subject, + Notification: shareSesssion.shareSessionNotification, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_builder_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_builder_test.go new file mode 100644 index 0000000..b0101e2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_builder_test.go @@ -0,0 +1,99 @@ +package digitalidentity + +import ( + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/extension" +) + +func ExampleShareSessionRequestBuilder() { + shareSession, err := (&ShareSessionRequestBuilder{}).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := shareSession.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"policy":{"wanted":null,"wanted_auth_types":null,"wanted_remember_me":false},"extensions":[],"redirectUri":""} +} + +func ExampleShareSessionRequestBuilder_WithPolicy() { + policy, err := (&PolicyBuilder{}).WithEmail().WithPinAuth().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + session, err := (&ShareSessionRequestBuilder{}).WithPolicy(policy).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := session.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"policy":{"wanted":[{"name":"email_address","accept_self_asserted":false}],"wanted_auth_types":[2],"wanted_remember_me":false},"extensions":[],"redirectUri":""} +} + +func ExampleShareSessionRequestBuilder_WithExtension() { + policy, err := (&PolicyBuilder{}).WithFullName().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + builtExtension, err := (&extension.TransactionalFlowExtensionBuilder{}). + WithContent("Transactional Flow Extension"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + session, err := (&ShareSessionRequestBuilder{}).WithExtension(builtExtension).WithPolicy(policy).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := session.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"policy":{"wanted":[{"name":"full_name","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false},"extensions":[{"type":"TRANSACTIONAL_FLOW","content":"Transactional Flow Extension"}],"redirectUri":""} +} + +func ExampleShareSessionRequestBuilder_WithSubject() { + subject := []byte(`{ + "subject_id": "some_subject_id_string" + }`) + + session, err := (&ShareSessionRequestBuilder{}).WithSubject(subject).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := session.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"policy":{"wanted":null,"wanted_auth_types":null,"wanted_remember_me":false},"extensions":[],"redirectUri":"","subject":{"subject_id":"some_subject_id_string"}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_created.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_created.go new file mode 100644 index 0000000..86d60e3 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_created.go @@ -0,0 +1,8 @@ +package digitalidentity + +// ShareSessionCreated Share Session QR Result +type ShareSessionCreated struct { + ID string `json:"id"` + Satus string `json:"status"` + Expiry string `json:"expiry"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_notification_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_notification_builder.go new file mode 100644 index 0000000..6c873eb --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_notification_builder.go @@ -0,0 +1,62 @@ +package digitalidentity + +import ( + "encoding/json" +) + +// ShareSessionNotification specifies the session notification configuration. +type ShareSessionNotification struct { + url string + method *string + verifyTLS *bool + headers map[string][]string +} + +// ShareSessionNotificationBuilder builds Share Session Notification +type ShareSessionNotificationBuilder struct { + shareSessionNotification ShareSessionNotification +} + +// WithUrl setsUrl to Share Session Notification +func (b *ShareSessionNotificationBuilder) WithUrl(url string) *ShareSessionNotificationBuilder { + b.shareSessionNotification.url = url + return b +} + +// WithMethod set method to Share Session Notification +func (b *ShareSessionNotificationBuilder) WithMethod(method string) *ShareSessionNotificationBuilder { + b.shareSessionNotification.method = &method + return b +} + +// WithVerifyTLS sets whether TLS should be verified for notifications. +func (b *ShareSessionNotificationBuilder) WithVerifyTls(verifyTls bool) *ShareSessionNotificationBuilder { + b.shareSessionNotification.verifyTLS = &verifyTls + return b +} + +// WithHeaders set headers to Share Session Notification +func (b *ShareSessionNotificationBuilder) WithHeaders(headers map[string][]string) *ShareSessionNotificationBuilder { + b.shareSessionNotification.headers = headers + return b +} + +// Build constructs the Share Session Notification Builder +func (b *ShareSessionNotificationBuilder) Build() (ShareSessionNotification, error) { + return b.shareSessionNotification, nil +} + +// MarshalJSON returns the JSON encoding +func (a *ShareSessionNotification) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Url string `json:"url"` + Method *string `json:"method,omitempty"` + VerifyTls *bool `json:"verifyTls,omitempty"` + Headers map[string][]string `json:"headers,omitempty"` + }{ + Url: a.url, + Method: a.method, + VerifyTls: a.verifyTLS, + Headers: a.headers, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_notification_builder_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_notification_builder_test.go new file mode 100644 index 0000000..a60e301 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_notification_builder_test.go @@ -0,0 +1,95 @@ +package digitalidentity + +import ( + "fmt" +) + +func ExampleShareSessionNotificationBuilder() { + shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := shareSessionNotify.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"url":""} +} + +func ExampleShareSessionNotificationBuilder_WithUrl() { + shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).WithUrl("Custom_Url").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := shareSessionNotify.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"url":"Custom_Url"} +} + +func ExampleShareSessionNotificationBuilder_WithMethod() { + shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).WithMethod("CUSTOMMETHOD").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := shareSessionNotify.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"url":"","method":"CUSTOMMETHOD"} +} + +func ExampleShareSessionNotificationBuilder_WithVerifyTls() { + + shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).WithVerifyTls(true).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := shareSessionNotify.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"url":"","verifyTls":true} +} + +func ExampleShareSessionNotificationBuilder_WithHeaders() { + + headers := make(map[string][]string) + headers["key"] = append(headers["key"], "value") + + shareSessionNotify, err := (&ShareSessionNotificationBuilder{}).WithHeaders(headers).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := shareSessionNotify.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"url":"","headers":{"key":["value"]}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_qr_code.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_qr_code.go new file mode 100644 index 0000000..5f98caa --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/share_session_qr_code.go @@ -0,0 +1,14 @@ +package digitalidentity + +type ShareSessionQrCode struct { + ID string `json:"id"` + Expiry string `json:"expiry"` + Policy string `json:"policy"` + Extensions []interface{} `json:"extensions"` + Session struct { + ID string `json:"id"` + Status string `json:"status"` + Expiry string `json:"expiry"` + } `json:"session"` + RedirectURI string `json:"redirectUri"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/source_constraint.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/source_constraint.go new file mode 100644 index 0000000..079007f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/source_constraint.go @@ -0,0 +1,105 @@ +package digitalidentity + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/yotierror" +) + +// Anchor name constants +const ( + AnchorDrivingLicenceConst = "DRIVING_LICENCE" + AnchorPassportConst = "PASSPORT" + AnchorNationalIDConst = "NATIONAL_ID" + AnchorPassCardConst = "PASS_CARD" +) + +// SourceConstraint describes a requirement or preference for a particular set +// of anchors +type SourceConstraint struct { + anchors []WantedAnchor + softPreference bool +} + +// SourceConstraintBuilder builds a source constraint +type SourceConstraintBuilder struct { + sourceConstraint SourceConstraint + err error +} + +// WithAnchorByValue is a helper method which builds an anchor and adds it to +// the source constraint +func (b *SourceConstraintBuilder) WithAnchorByValue(value, subtype string) *SourceConstraintBuilder { + anchor, err := (&WantedAnchorBuilder{}). + WithValue(value). + WithSubType(subtype). + Build() + if err != nil { + b.err = yotierror.MultiError{This: err, Next: b.err} + } + + return b.WithAnchor(anchor) +} + +// WithAnchor adds an anchor to the preference list +func (b *SourceConstraintBuilder) WithAnchor(anchor WantedAnchor) *SourceConstraintBuilder { + b.sourceConstraint.anchors = append(b.sourceConstraint.anchors, anchor) + return b +} + +// WithPassport adds a passport anchor +func (b *SourceConstraintBuilder) WithPassport(subtype string) *SourceConstraintBuilder { + return b.WithAnchorByValue(AnchorPassportConst, subtype) +} + +// WithDrivingLicence adds a Driving Licence anchor +func (b *SourceConstraintBuilder) WithDrivingLicence(subtype string) *SourceConstraintBuilder { + return b.WithAnchorByValue(AnchorDrivingLicenceConst, subtype) +} + +// WithNationalID adds a national ID anchor +func (b *SourceConstraintBuilder) WithNationalID(subtype string) *SourceConstraintBuilder { + return b.WithAnchorByValue(AnchorNationalIDConst, subtype) +} + +// WithPasscard adds a passcard anchor +func (b *SourceConstraintBuilder) WithPasscard(subtype string) *SourceConstraintBuilder { + return b.WithAnchorByValue(AnchorPassCardConst, subtype) +} + +// WithSoftPreference sets this constraint as a 'soft requirement' if the +// parameter is true, and a hard requirement if it is false. +func (b *SourceConstraintBuilder) WithSoftPreference(soft bool) *SourceConstraintBuilder { + b.sourceConstraint.softPreference = soft + return b +} + +// Build builds a SourceConstraint +func (b *SourceConstraintBuilder) Build() (SourceConstraint, error) { + if b.sourceConstraint.anchors == nil { + b.sourceConstraint.anchors = make([]WantedAnchor, 0) + } + return b.sourceConstraint, b.err +} + +func (constraint *SourceConstraint) isConstraint() bool { + return true +} + +// MarshalJSON returns the JSON encoding +func (constraint *SourceConstraint) MarshalJSON() ([]byte, error) { + type PreferenceList struct { + Anchors []WantedAnchor `json:"anchors"` + SoftPreference bool `json:"soft_preference"` + } + return json.Marshal(&struct { + Type string `json:"type"` + PreferredSources PreferenceList `json:"preferred_sources"` + }{ + Type: "SOURCE", + PreferredSources: PreferenceList{ + Anchors: constraint.anchors, + SoftPreference: constraint.softPreference, + }, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/user_profile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/user_profile.go new file mode 100644 index 0000000..32a4d28 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/user_profile.go @@ -0,0 +1,182 @@ +package digitalidentity + +import ( + "strings" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// UserProfile represents the details retrieved for a particular user. Consists of +// Yoti attributes: a small piece of information about a Yoti user such as a +// photo of the user or the user's date of birth. +type UserProfile struct { + baseProfile +} + +// Creates a new Profile struct +func newUserProfile(attributes *yotiprotoattr.AttributeList) UserProfile { + return UserProfile{ + baseProfile{ + attributeSlice: createAttributeSlice(attributes), + }, + } +} + +func createAttributeSlice(protoAttributeList *yotiprotoattr.AttributeList) (result []*yotiprotoattr.Attribute) { + if protoAttributeList != nil { + result = append(result, protoAttributeList.Attributes...) + } + + return result +} + +// Selfie is a photograph of the user. Will be nil if not provided by Yoti. +func (p UserProfile) Selfie() *attribute.ImageAttribute { + return p.GetImageAttribute(consts.AttrSelfie) +} + +// GetSelfieAttributeByID retrieve a Selfie attribute by ID on the Yoti profile. +// This attribute is a photograph of the user. +// Will return nil if attribute is not present. +func (p UserProfile) GetSelfieAttributeByID(attributeID string) (*attribute.ImageAttribute, error) { + for _, a := range p.attributeSlice { + if a.EphemeralId == attributeID { + return attribute.NewImage(a) + } + } + return nil, nil +} + +// GivenNames corresponds to secondary names in passport, and first/middle names in English. Will be nil if not provided by Yoti. +func (p UserProfile) GivenNames() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrGivenNames) +} + +// FamilyName corresponds to primary name in passport, and surname in English. Will be nil if not provided by Yoti. +func (p UserProfile) FamilyName() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrFamilyName) +} + +// FullName represents the user's full name. +// If family_name/given_names are present, the value will be equal to the string 'given_names + " " family_name'. +// Will be nil if not provided by Yoti. +func (p UserProfile) FullName() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrFullName) +} + +// MobileNumber represents the user's mobile phone number, as verified at registration time. +// The value will be a number in E.164 format (i.e. '+' for international prefix and no spaces, e.g. "+447777123456"). +// Will be nil if not provided by Yoti. +func (p UserProfile) MobileNumber() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrMobileNumber) +} + +// EmailAddress represents the user's verified email address. Will be nil if not provided by Yoti. +func (p UserProfile) EmailAddress() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrEmailAddress) +} + +// DateOfBirth represents the user's date of birth. Will be nil if not provided by Yoti. +// Has an err value which will be filled if there is an error parsing the date. +func (p UserProfile) DateOfBirth() (*attribute.DateAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == consts.AttrDateOfBirth { + return attribute.NewDate(a) + } + } + return nil, nil +} + +// Address represents the user's address. Will be nil if not provided by Yoti. +func (p UserProfile) Address() *attribute.StringAttribute { + addressAttribute := p.GetStringAttribute(consts.AttrAddress) + if addressAttribute == nil { + return ensureAddressProfile(&p) + } + + return addressAttribute +} + +// StructuredPostalAddress represents the user's address in a JSON format. +// Will be nil if not provided by Yoti. This can be accessed as a +// map[string]string{} using a type assertion, e.g.: +// structuredPostalAddress := structuredPostalAddressAttribute.Value().(map[string]string{}) +func (p UserProfile) StructuredPostalAddress() (*attribute.JSONAttribute, error) { + return p.GetJSONAttribute(consts.AttrStructuredPostalAddress) +} + +// Gender corresponds to the gender in the registered document; the value will be one of the strings "MALE", "FEMALE", "TRANSGENDER" or "OTHER". +// Will be nil if not provided by Yoti. +func (p UserProfile) Gender() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrGender) +} + +// Nationality corresponds to the nationality in the passport. +// The value is an ISO-3166-1 alpha-3 code with ICAO9303 (passport) extensions. +// Will be nil if not provided by Yoti. +func (p UserProfile) Nationality() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrNationality) +} + +// DocumentImages returns a slice of document images cropped from the image in the capture page. +// There can be multiple images as per the number of regions in the capture in this attribute. +// Will be nil if not provided by Yoti. +func (p UserProfile) DocumentImages() (*attribute.ImageSliceAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == consts.AttrDocumentImages { + return attribute.NewImageSlice(a) + } + } + return nil, nil +} + +// GetDocumentImagesAttributeByID retrieve a Document Images attribute by ID on the Yoti profile. +// This attribute consists of a slice of document images cropped from the image in the capture page. +// There can be multiple images as per the number of regions in the capture in this attribute. +// Will return nil if attribute is not present. +func (p UserProfile) GetDocumentImagesAttributeByID(attributeID string) (*attribute.ImageSliceAttribute, error) { + for _, a := range p.attributeSlice { + if a.EphemeralId == attributeID { + return attribute.NewImageSlice(a) + } + } + return nil, nil +} + +// DocumentDetails represents information extracted from a document provided by the user. +// Will be nil if not provided by Yoti. +func (p UserProfile) DocumentDetails() (*attribute.DocumentDetailsAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == consts.AttrDocumentDetails { + return attribute.NewDocumentDetails(a) + } + } + return nil, nil +} + +// IdentityProfileReport represents the JSON object containing identity assertion and the +// verification report. Will be nil if not provided by Yoti. +func (p UserProfile) IdentityProfileReport() (*attribute.JSONAttribute, error) { + return p.GetJSONAttribute(consts.AttrIdentityProfileReport) +} + +// AgeVerifications returns a slice of age verifications for the user. +// Will be an empty slice if not provided by Yoti. +func (p UserProfile) AgeVerifications() (out []attribute.AgeVerification, err error) { + ageUnderString := strings.Replace(consts.AttrAgeUnder, "%d", "", -1) + ageOverString := strings.Replace(consts.AttrAgeOver, "%d", "", -1) + + for _, a := range p.attributeSlice { + if strings.HasPrefix(a.Name, ageUnderString) || + strings.HasPrefix(a.Name, ageOverString) { + verification, err := attribute.NewAgeVerification(a) + if err != nil { + return nil, err + } + out = append(out, verification) + } + } + return out, err +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/user_profile_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/user_profile_test.go new file mode 100644 index 0000000..b81b78e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/user_profile_test.go @@ -0,0 +1,704 @@ +package digitalidentity + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/file" + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +const ( + attributeName = "test_attribute_name" + attributeValueString = "value" + + documentImagesAttributeID = "document-images-attribute-id-123" + selfieAttributeID = "selfie-attribute-id-123" + fullNameAttributeID = "full-name-id-123" +) + +var attributeValue = []byte(attributeValueString) + +func getUserProfile() UserProfile { + userProfile := createProfileWithMultipleAttributes( + createDocumentImagesAttribute(documentImagesAttributeID), + createSelfieAttribute(yotiprotoattr.ContentType_JPEG, selfieAttributeID), + createStringAttribute("full_name", []byte("John Smith"), []*yotiprotoattr.Anchor{}, fullNameAttributeID)) + + return userProfile +} + +func ExampleUserProfile_GetAttributeByID() { + userProfile := getUserProfile() + fullNameAttribute := userProfile.GetAttributeByID("full-name-id-123") + value := fullNameAttribute.Value().(string) + + fmt.Println(value) + // Output: John Smith +} + +func ExampleUserProfile_GetDocumentImagesAttributeByID() { + userProfile := getUserProfile() + documentImagesAttribute, err := userProfile.GetDocumentImagesAttributeByID("document-images-attribute-id-123") + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(*documentImagesAttribute.ID()) + // Output: document-images-attribute-id-123 +} + +func ExampleUserProfile_GetSelfieAttributeByID() { + userProfile := getUserProfile() + selfieAttribute, err := userProfile.GetSelfieAttributeByID("selfie-attribute-id-123") + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(*selfieAttribute.ID()) + // Output: selfie-attribute-id-123 +} + +func createProfileWithSingleAttribute(attr *yotiprotoattr.Attribute) UserProfile { + var attributeSlice []*yotiprotoattr.Attribute + attributeSlice = append(attributeSlice, attr) + + return UserProfile{ + baseProfile{ + attributeSlice: attributeSlice, + }, + } +} + +func createAppProfileWithSingleAttribute(attr *yotiprotoattr.Attribute) ApplicationProfile { + var attributeSlice []*yotiprotoattr.Attribute + attributeSlice = append(attributeSlice, attr) + + return ApplicationProfile{ + baseProfile{ + attributeSlice: attributeSlice, + }, + } +} + +func createProfileWithMultipleAttributes(list ...*yotiprotoattr.Attribute) UserProfile { + return UserProfile{ + baseProfile{ + attributeSlice: list, + }, + } +} + +func TestProfile_AgeVerifications(t *testing.T) { + ageOver14 := &yotiprotoattr.Attribute{ + Name: "age_over:14", + Value: []byte("true"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + ageUnder18 := &yotiprotoattr.Attribute{ + Name: "age_under:18", + Value: []byte("true"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + ageOver18 := &yotiprotoattr.Attribute{ + Name: "age_over:18", + Value: []byte("false"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithMultipleAttributes(ageOver14, ageUnder18, ageOver18) + ageVerifications, err := profile.AgeVerifications() + + assert.NilError(t, err) + assert.Equal(t, len(ageVerifications), 3) + + assert.Equal(t, ageVerifications[0].Age, 14) + assert.Equal(t, ageVerifications[0].CheckType, "age_over") + assert.Equal(t, ageVerifications[0].Result, true) + + assert.Equal(t, ageVerifications[1].Age, 18) + assert.Equal(t, ageVerifications[1].CheckType, "age_under") + assert.Equal(t, ageVerifications[1].Result, true) + + assert.Equal(t, ageVerifications[2].Age, 18) + assert.Equal(t, ageVerifications[2].CheckType, "age_over") + assert.Equal(t, ageVerifications[2].Result, false) +} + +func TestProfile_GetAttribute_EmptyString(t *testing.T) { + emptyString := "" + attributeValue := []byte(emptyString) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + assert.Equal(t, att.Name(), attributeName) + assert.Equal(t, att.Value().(string), emptyString) +} + +func TestProfile_GetApplicationAttribute(t *testing.T) { + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createProfileWithSingleAttribute(attr) + applicationAttribute := appProfile.GetAttribute(attributeName) + assert.Equal(t, applicationAttribute.Name(), attributeName) +} + +func TestProfile_GetApplicationName(t *testing.T) { + attributeValue := "APPLICATION NAME" + var attr = &yotiprotoattr.Attribute{ + Name: "application_name", + Value: []byte(attributeValue), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createAppProfileWithSingleAttribute(attr) + assert.Equal(t, attributeValue, appProfile.ApplicationName().Value()) +} + +func TestProfile_GetApplicationURL(t *testing.T) { + attributeValue := "APPLICATION URL" + var attr = &yotiprotoattr.Attribute{ + Name: "application_url", + Value: []byte(attributeValue), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createAppProfileWithSingleAttribute(attr) + assert.Equal(t, attributeValue, appProfile.ApplicationURL().Value()) +} + +func TestProfile_GetApplicationLogo(t *testing.T) { + attributeValue := "APPLICATION LOGO" + var attr = &yotiprotoattr.Attribute{ + Name: "application_logo", + Value: []byte(attributeValue), + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createAppProfileWithSingleAttribute(attr) + assert.Equal(t, 16, len(appProfile.ApplicationLogo().Value().Data())) +} + +func TestProfile_GetApplicationBGColor(t *testing.T) { + attributeValue := "BG VALUE" + var attr = &yotiprotoattr.Attribute{ + Name: "application_receipt_bgcolor", + Value: []byte(attributeValue), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createAppProfileWithSingleAttribute(attr) + assert.Equal(t, attributeValue, appProfile.ApplicationReceiptBgColor().Value()) +} + +func TestProfile_GetAttribute_Int(t *testing.T) { + intValues := [5]int{0, 1, 123, -10, -1} + + for _, integer := range intValues { + assertExpectedIntegerIsReturned(t, integer) + } +} + +func assertExpectedIntegerIsReturned(t *testing.T, intValue int) { + intAsString := strconv.Itoa(intValue) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: []byte(intAsString), + ContentType: yotiprotoattr.ContentType_INT, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + assert.Equal(t, att.Value().(int), intValue) +} + +func TestProfile_GetAttribute_InvalidInt_ReturnsNil(t *testing.T) { + invalidIntValue := "1985-01-01" + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: []byte(invalidIntValue), + ContentType: yotiprotoattr.ContentType_INT, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + + att := result.GetAttribute(attributeName) + + assert.Assert(t, is.Nil(att)) +} + +func TestProfile_EmptyStringIsAllowed(t *testing.T) { + emptyString := "" + attrValue := []byte(emptyString) + + var attr = &yotiprotoattr.Attribute{ + Name: consts.AttrGender, + Value: attrValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(attr) + att := profile.Gender() + + assert.Equal(t, att.Value(), emptyString) +} + +func TestProfile_GetAttribute_Time(t *testing.T) { + dateStringValue := "1985-01-01" + expectedDate := time.Date(1985, time.January, 1, 0, 0, 0, 0, time.UTC) + + attributeValueTime := []byte(dateStringValue) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValueTime, + ContentType: yotiprotoattr.ContentType_DATE, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + assert.Equal(t, expectedDate, att.Value().(*time.Time).UTC()) +} + +func TestProfile_GetAttribute_Jpeg(t *testing.T) { + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(attr) + att := profile.GetAttribute(attributeName) + + expected := media.JPEGImage(attributeValue) + result := att.Value().(media.JPEGImage) + + assert.DeepEqual(t, expected, result) + assert.Equal(t, expected.Base64URL(), result.Base64URL()) +} + +func TestProfile_GetAttribute_Png(t *testing.T) { + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(attr) + att := profile.GetAttribute(attributeName) + + expected := media.PNGImage(attributeValue) + result := att.Value().(media.PNGImage) + + assert.DeepEqual(t, expected, result) + assert.Equal(t, expected.Base64URL(), result.Base64URL()) +} + +func TestProfile_GetAttribute_Bool(t *testing.T) { + var initialBoolValue = true + attrValue := []byte(strconv.FormatBool(initialBoolValue)) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attrValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + boolValue, err := strconv.ParseBool(att.Value().(string)) + + assert.NilError(t, err) + assert.Equal(t, initialBoolValue, boolValue) +} + +func TestProfile_GetAttribute_JSON(t *testing.T) { + addressFormat := "2" + + var structuredAddressBytes = []byte(` + { + "address_format": "` + addressFormat + `", + "building": "House No.86-A" + }`) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: structuredAddressBytes, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + retrievedAttributeMap := att.Value().(map[string]interface{}) + actualAddressFormat := retrievedAttributeMap["address_format"] + + assert.Equal(t, actualAddressFormat, addressFormat) +} + +func TestProfile_GetAttribute_Undefined(t *testing.T) { + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + assert.Equal(t, att.Name(), attributeName) + assert.Equal(t, att.Value().(string), attributeValueString) +} + +func TestProfile_GetAttribute_ReturnsNil(t *testing.T) { + userProfile := UserProfile{ + baseProfile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + }, + } + + result := userProfile.GetAttribute("attributeName") + + assert.Assert(t, is.Nil(result)) +} + +func TestProfile_GetAttributeByID(t *testing.T) { + attributeID := "att-id-123" + + var attr1 = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + EphemeralId: attributeID, + } + var attr2 = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + EphemeralId: "non-matching-attribute-ID", + } + + profile := createProfileWithMultipleAttributes(attr1, attr2) + + result := profile.GetAttributeByID(attributeID) + assert.DeepEqual(t, result.ID(), &attributeID) +} + +func TestProfile_GetAttributeByID_ReturnsNil(t *testing.T) { + userProfile := UserProfile{ + baseProfile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + }, + } + + result := userProfile.GetAttributeByID("attributeName") + + assert.Assert(t, is.Nil(result)) +} + +func TestProfile_GetDocumentImagesAttributeByID_ReturnsNil(t *testing.T) { + userProfile := UserProfile{ + baseProfile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + }, + } + + result, err := userProfile.GetDocumentImagesAttributeByID("attributeName") + assert.NilError(t, err) + assert.Assert(t, is.Nil(result)) +} + +func TestProfile_GetSelfieAttributeByID_ReturnsNil(t *testing.T) { + userProfile := UserProfile{ + baseProfile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + }, + } + + result, err := userProfile.GetSelfieAttributeByID("attributeName") + assert.NilError(t, err) + assert.Assert(t, is.Nil(result)) +} + +func TestProfile_StringAttribute(t *testing.T) { + nationalityName := consts.AttrNationality + + var as = &yotiprotoattr.Attribute{ + Name: nationalityName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(as) + + assert.Equal(t, result.Nationality().Value(), attributeValueString) + + assert.Equal(t, result.Nationality().ContentType(), yotiprotoattr.ContentType_STRING.String()) +} + +func TestProfile_AttributeProperty_RetrievesAttribute(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + assert.Equal(t, selfie.Name(), consts.AttrSelfie) + assert.DeepEqual(t, attributeValue, selfie.Value().Data()) + assert.Equal(t, selfie.ContentType(), yotiprotoattr.ContentType_PNG.String()) +} + +func TestProfile_DocumentDetails_RetrievesAttribute(t *testing.T) { + documentDetailsName := consts.AttrDocumentDetails + attributeValue := []byte("PASSPORT GBR 1234567") + + var protoAttribute = &yotiprotoattr.Attribute{ + Name: documentDetailsName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: make([]*yotiprotoattr.Anchor, 0), + } + + result := createProfileWithSingleAttribute(protoAttribute) + documentDetails, err := result.DocumentDetails() + assert.NilError(t, err) + + assert.Equal(t, documentDetails.Value().DocumentType, "PASSPORT") +} + +func TestProfile_DocumentImages_RetrievesAttribute(t *testing.T) { + protoAttribute := createDocumentImagesAttribute("attr-id") + + result := createProfileWithSingleAttribute(protoAttribute) + documentImages, err := result.DocumentImages() + assert.NilError(t, err) + + assert.Equal(t, documentImages.Name(), consts.AttrDocumentImages) +} + +func TestProfile_AttributesReturnsNilWhenNotPresent(t *testing.T) { + documentImagesName := consts.AttrDocumentImages + multiValue, err := proto.Marshal(&yotiprotoattr.MultiValue{}) + assert.NilError(t, err) + + protoAttribute := &yotiprotoattr.Attribute{ + Name: documentImagesName, + Value: multiValue, + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Anchors: make([]*yotiprotoattr.Anchor, 0), + } + + result := createProfileWithSingleAttribute(protoAttribute) + + DoB, err := result.DateOfBirth() + assert.Check(t, DoB == nil) + assert.Check(t, err == nil) + assert.Check(t, result.Address() == nil) +} + +func TestMissingPostalAddress_UsesFormattedAddress(t *testing.T) { + var formattedAddressText = `House No.86-A\nRajgura Nagar\nLudhina\nPunjab\n141012\nIndia` + + var structuredAddressBytes = []byte(` + { + "address_format": 2, + "building": "House No.86-A", + "formatted_address": "` + formattedAddressText + `" + } + `) + + var jsonAttribute = &yotiprotoattr.Attribute{ + Name: consts.AttrStructuredPostalAddress, + Value: structuredAddressBytes, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(jsonAttribute) + + ensureAddressProfile(&profile) + + escapedFormattedAddressText := strings.Replace(formattedAddressText, `\n`, "\n", -1) + + profileAddress := profile.Address().Value() + assert.Equal(t, profileAddress, escapedFormattedAddressText, "Address does not equal the expected formatted address.") + + structuredPostalAddress, err := profile.StructuredPostalAddress() + assert.NilError(t, err) + assert.Equal(t, structuredPostalAddress.ContentType(), "JSON") +} + +func TestAttributeImage_Image_Png(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + assert.DeepEqual(t, selfie.Value().Data(), attributeValue) +} + +func TestAttributeImage_Image_Jpeg(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_JPEG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + assert.DeepEqual(t, selfie.Value().Data(), attributeValue) +} + +func TestAttributeImage_Image_Default(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + assert.DeepEqual(t, selfie.Value().Data(), attributeValue) +} +func TestAttributeImage_Base64Selfie_Png(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(attributeValue) + expectedBase64Selfie := "data:image/png;base64," + base64ImageExpectedValue + base64Selfie := result.Selfie().Value().Base64URL() + + assert.Equal(t, base64Selfie, expectedBase64Selfie) +} + +func TestAttributeImage_Base64URL_Jpeg(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_JPEG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(attributeValue) + + expectedBase64Selfie := "data:image/jpeg;base64," + base64ImageExpectedValue + + base64Selfie := result.Selfie().Value().Base64URL() + + assert.Equal(t, base64Selfie, expectedBase64Selfie) +} + +func TestProfile_IdentityProfileReport_RetrievesAttribute(t *testing.T) { + identityProfileReportJSON, err := file.ReadFile("../test/fixtures/RTWIdentityProfileReport.json") + assert.NilError(t, err) + + var attr = &yotiprotoattr.Attribute{ + Name: consts.AttrIdentityProfileReport, + Value: identityProfileReportJSON, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att, err := result.IdentityProfileReport() + assert.NilError(t, err) + + retrievedIdentityProfile := att.Value() + gotProof := retrievedIdentityProfile["proof"] + + assert.Equal(t, gotProof, "") +} + +func TestProfileAllowsMultipleAttributesWithSameName(t *testing.T) { + firstAttribute := createStringAttribute("full_name", []byte("some_value"), []*yotiprotoattr.Anchor{}, "id") + secondAttribute := createStringAttribute("full_name", []byte("some_other_value"), []*yotiprotoattr.Anchor{}, "id") + + var attributeSlice []*yotiprotoattr.Attribute + attributeSlice = append(attributeSlice, firstAttribute, secondAttribute) + + var profile = UserProfile{ + baseProfile{ + attributeSlice: attributeSlice, + }, + } + + var fullNames = profile.GetAttributes("full_name") + + assert.Assert(t, is.Equal(len(fullNames), 2)) + assert.Assert(t, is.Equal(fullNames[0].Value().(string), "some_value")) + assert.Assert(t, is.Equal(fullNames[1].Value().(string), "some_other_value")) +} + +func createStringAttribute(name string, value []byte, anchors []*yotiprotoattr.Anchor, attributeID string) *yotiprotoattr.Attribute { + return &yotiprotoattr.Attribute{ + Name: name, + Value: value, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: anchors, + EphemeralId: attributeID, + } +} + +func createSelfieAttribute(contentType yotiprotoattr.ContentType, attributeID string) *yotiprotoattr.Attribute { + var attributeImage = &yotiprotoattr.Attribute{ + Name: consts.AttrSelfie, + Value: attributeValue, + ContentType: contentType, + Anchors: []*yotiprotoattr.Anchor{}, + EphemeralId: attributeID, + } + return attributeImage +} + +func createDocumentImagesAttribute(attributeID string) *yotiprotoattr.Attribute { + multiValue, err := proto.Marshal(&yotiprotoattr.MultiValue{}) + if err != nil { + panic(err) + } + + protoAttribute := &yotiprotoattr.Attribute{ + Name: consts.AttrDocumentImages, + Value: multiValue, + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Anchors: make([]*yotiprotoattr.Anchor, 0), + EphemeralId: attributeID, + } + return protoAttribute +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_anchor_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_anchor_builder.go new file mode 100644 index 0000000..855c9c3 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_anchor_builder.go @@ -0,0 +1,44 @@ +package digitalidentity + +import ( + "encoding/json" +) + +// WantedAnchor specifies a preferred anchor for a user's details +type WantedAnchor struct { + name string + subType string +} + +// WantedAnchorBuilder describes a desired anchor for user profile data +type WantedAnchorBuilder struct { + wantedAnchor WantedAnchor +} + +// WithValue sets the anchor's name +func (b *WantedAnchorBuilder) WithValue(name string) *WantedAnchorBuilder { + b.wantedAnchor.name = name + return b +} + +// WithSubType sets the anchors subtype +func (b *WantedAnchorBuilder) WithSubType(subType string) *WantedAnchorBuilder { + b.wantedAnchor.subType = subType + return b +} + +// Build constructs the anchor from the builder's specification +func (b *WantedAnchorBuilder) Build() (WantedAnchor, error) { + return b.wantedAnchor, nil +} + +// MarshalJSON ... +func (a *WantedAnchor) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Name string `json:"name"` + SubType string `json:"sub_type"` + }{ + Name: a.name, + SubType: a.subType, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_anchor_builder_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_anchor_builder_test.go new file mode 100644 index 0000000..907d8e0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_anchor_builder_test.go @@ -0,0 +1,25 @@ +package digitalidentity + +import ( + "fmt" +) + +func ExampleWantedAnchorBuilder() { + aadhaarAnchor, err := (&WantedAnchorBuilder{}). + WithValue("NATIONAL_ID"). + WithSubType("AADHAAR"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + aadhaarJSON, err := aadhaarAnchor.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println("Aadhaar:", string(aadhaarJSON)) + // Output: Aadhaar: {"name":"NATIONAL_ID","sub_type":"AADHAAR"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_attribute_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_attribute_builder.go new file mode 100644 index 0000000..d004b96 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_attribute_builder.go @@ -0,0 +1,81 @@ +package digitalidentity + +import ( + "encoding/json" + "errors" +) + +type constraintInterface interface { + MarshalJSON() ([]byte, error) + isConstraint() bool // This function is not used but makes inheritance explicit +} + +// WantedAttributeBuilder generates the payload for specifying a single wanted +// attribute as part of a dynamic scenario +type WantedAttributeBuilder struct { + attr WantedAttribute +} + +// WantedAttribute represents a wanted attribute in a dynamic sharing policy +type WantedAttribute struct { + name string + derivation string + constraints []constraintInterface + acceptSelfAsserted bool + Optional bool +} + +// WithName sets the name of the wanted attribute +func (builder *WantedAttributeBuilder) WithName(name string) *WantedAttributeBuilder { + builder.attr.name = name + return builder +} + +// WithDerivation sets the derivation +func (builder *WantedAttributeBuilder) WithDerivation(derivation string) *WantedAttributeBuilder { + builder.attr.derivation = derivation + return builder +} + +// WithConstraint adds a constraint to a wanted attribute +func (builder *WantedAttributeBuilder) WithConstraint(constraint constraintInterface) *WantedAttributeBuilder { + builder.attr.constraints = append(builder.attr.constraints, constraint) + return builder +} + +// WithAcceptSelfAsserted allows self-asserted user details, such as those from Aadhar +func (builder *WantedAttributeBuilder) WithAcceptSelfAsserted(accept bool) *WantedAttributeBuilder { + builder.attr.acceptSelfAsserted = accept + return builder +} + +// Build generates the wanted attribute's specification +func (builder *WantedAttributeBuilder) Build() (WantedAttribute, error) { + if builder.attr.constraints == nil { + builder.attr.constraints = make([]constraintInterface, 0) + } + + var err error + if len(builder.attr.name) == 0 { + err = errors.New("wanted attribute names must not be empty") + } + + return builder.attr, err +} + +// MarshalJSON returns the JSON encoding +func (attr *WantedAttribute) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Name string `json:"name"` + Derivation string `json:"derivation,omitempty"` + Constraints []constraintInterface `json:"constraints,omitempty"` + AcceptSelfAsserted bool `json:"accept_self_asserted"` + Optional bool `json:"optional,omitempty"` + }{ + Name: attr.name, + Derivation: attr.derivation, + Constraints: attr.constraints, + AcceptSelfAsserted: attr.acceptSelfAsserted, + Optional: attr.Optional, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_attribute_test.go new file mode 100644 index 0000000..0a990d1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/wanted_attribute_test.go @@ -0,0 +1,154 @@ +package digitalidentity + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleWantedAttributeBuilder_WithName() { + builder := (&WantedAttributeBuilder{}).WithName("TEST NAME") + attribute, err := builder.Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(attribute.name) + // Output: TEST NAME +} + +func ExampleWantedAttributeBuilder_WithDerivation() { + attribute, err := (&WantedAttributeBuilder{}). + WithDerivation("TEST DERIVATION"). + WithName("TEST NAME"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(attribute.derivation) + // Output: TEST DERIVATION +} + +func ExampleWantedAttributeBuilder_WithConstraint() { + constraint, err := (&SourceConstraintBuilder{}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + WithConstraint(&constraint). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"TEST NAME","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[],"soft_preference":false}}],"accept_self_asserted":false} +} + +func ExampleWantedAttributeBuilder_WithAcceptSelfAsserted() { + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + WithAcceptSelfAsserted(true). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"TEST NAME","accept_self_asserted":true} +} + +func ExampleWantedAttributeBuilder_WithAcceptSelfAsserted_false() { + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + WithAcceptSelfAsserted(false). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"TEST NAME","accept_self_asserted":false} +} + +func ExampleWantedAttributeBuilder_optional_true() { + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + WithAcceptSelfAsserted(false). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + attribute.Optional = true + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"TEST NAME","accept_self_asserted":false,"optional":true} +} + +func TestWantedAttributeBuilder_Optional_IsOmittedByDefault(t *testing.T) { + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + Build() + if err != nil { + t.Errorf("error: %s", err.Error()) + } + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + t.Errorf("error: %s", err.Error()) + } + + attributeMap := unmarshalJSONIntoMap(t, marshalledJSON) + + optional := attributeMap["optional"] + + if optional != nil { + t.Errorf("expected `optional` to be nil, but was: '%v'", optional) + } +} + +func unmarshalJSONIntoMap(t *testing.T, byteValue []byte) (result map[string]interface{}) { + var unmarshalled interface{} + err := json.Unmarshal(byteValue, &unmarshalled) + assert.NilError(t, err) + + return unmarshalled.(map[string]interface{}) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/response.go new file mode 100644 index 0000000..3274e80 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/response.go @@ -0,0 +1,56 @@ +package yotierror + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +var ( + defaultUnknownErrorCodeConst = "UNKNOWN_ERROR" + defaultUnknownErrorMessageConst = "unknown HTTP error" +) + +// Error indicates errors related to the Yoti API. +type Error struct { + Id string `json:"id"` + Status int `json:"status"` + ErrorCode string `json:"error"` + Message string `json:"message"` +} + +func (e Error) Error() string { + return e.ErrorCode + " - " + e.Message +} + +// NewResponseError creates a new Error +func NewResponseError(response *http.Response) *Error { + err := &Error{ + ErrorCode: defaultUnknownErrorCodeConst, + Message: defaultUnknownErrorMessageConst, + } + if response == nil { + return err + } + err.Status = response.StatusCode + if response.Body == nil { + return err + } + defer response.Body.Close() + b, e := io.ReadAll(response.Body) + if e != nil { + err.Message = fmt.Sprintf(defaultUnknownErrorMessageConst+": %q", e) + return err + } + e = json.Unmarshal(b, err) + if e != nil { + err.Message = fmt.Sprintf(defaultUnknownErrorMessageConst+": %q", e) + } + return err +} + +// Temporary indicates this ErrorCode is a temporary ErrorCode +func (e Error) Temporary() bool { + return e.Status >= 500 +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/response_test.go new file mode 100644 index 0000000..be2225c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/response_test.go @@ -0,0 +1,68 @@ +package yotierror + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +var ( + expectedErr = Error{ + Id: "8f6a9dfe72128de20909af0d476769b6", + Status: 401, + ErrorCode: "INVALID_REQUEST_SIGNATURE", + Message: "Invalid request signature", + } +) + +func TestError_ShouldReturnFormattedError(t *testing.T) { + jsonBytes := json.RawMessage(`{"id":"8f6a9dfe72128de20909af0d476769b6","status":401,"error":"INVALID_REQUEST_SIGNATURE","message":"Invalid request signature"}`) + + err := NewResponseError( + &http.Response{ + StatusCode: 401, + Body: io.NopCloser(bytes.NewReader(jsonBytes)), + }, + ) + + assert.ErrorIs(t, *err, expectedErr) +} + +func TestError_ShouldReturnFormattedError_ReturnWrappedErrorWhenInvalidJSON(t *testing.T) { + response := &http.Response{ + StatusCode: 400, + Body: io.NopCloser(strings.NewReader("some invalid JSON")), + } + err := NewResponseError( + response, + ) + + assert.ErrorContains(t, err, "unknown HTTP error") +} + +func TestError_ShouldReturnTemporaryForServerError(t *testing.T) { + response := &http.Response{ + StatusCode: 500, + } + err := NewResponseError( + response, + ) + + assert.Check(t, err.Temporary()) +} + +func TestError_ShouldNotReturnTemporaryForClientError(t *testing.T) { + response := &http.Response{ + StatusCode: 400, + } + err := NewResponseError( + response, + ) + + assert.Check(t, !err.Temporary()) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/signed_requests.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/signed_requests.go new file mode 100644 index 0000000..3f91d38 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/digitalidentity/yotierror/signed_requests.go @@ -0,0 +1,8 @@ +package yotierror + +const ( + // InvalidRequestSignature can be returned by any endpoint that requires a signed request. + InvalidRequestSignature = "INVALID_REQUEST_SIGNATURE" + // InvalidAuthHeader can be returned by any endpoint that requires a signed request. + InvalidAuthHeader = "INVALID_AUTH_HEADER" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/client.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/client.go new file mode 100644 index 0000000..2d8f7ae --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/client.go @@ -0,0 +1,438 @@ +package docscan + +import ( + "crypto/rsa" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/facecapture" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/retrieve" + "github.com/getyoti/yoti-go-sdk/v3/docscan/supported" + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/requests" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" +) + +// Client is responsible for setting up test data in the sandbox instance. +type Client struct { + // SDK ID. This can be found in the Yoti Hub after you have created and activated an application. + SdkID string + // Private Key associated for your application, can be downloaded from the Yoti Hub. + Key *rsa.PrivateKey + // Mockable HTTP Client Interface + HTTPClient requests.HttpClient + // API URL to use. This is not required, and a default will be set if not provided. + apiURL string + // Mockable JSON marshaler + jsonMarshaler jsonMarshaler +} + +var mustNotBeEmptyString = "%s cannot be an empty string" + +// NewClient constructs a Client object +func NewClient(sdkID string, key []byte) (*Client, error) { + if sdkID == "" { + return nil, fmt.Errorf(mustNotBeEmptyString, "SdkID") + } + + decodedKey, err := cryptoutil.ParseRSAKey(key) + if err != nil { + return nil, err + } + + return &Client{ + SdkID: sdkID, + Key: decodedKey, + HTTPClient: http.DefaultClient, + apiURL: getAPIURL(), + }, err +} + +// OverrideAPIURL overrides the default API URL for this Yoti Client +func (c *Client) OverrideAPIURL(apiURL string) { + c.apiURL = apiURL +} + +func getAPIURL() string { + if value, exists := os.LookupEnv("YOTI_DOC_SCAN_API_URL"); exists && value != "" { + return value + } else { + return "https://api.yoti.com/idverify/v1" + } +} + +// CreateSession creates a Doc Scan (IDV) session using the supplied session specification +func (c *Client) CreateSession(sessionSpec *create.SessionSpecification) (*create.SessionResult, error) { + requestBody, err := marshalJSON(c.jsonMarshaler, sessionSpec) + if err != nil { + return nil, err + } + + var request *http.Request + request, err = (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodPost, + BaseURL: c.apiURL, + Endpoint: createSessionPath(), + Headers: requests.JSONHeaders(), + Body: requestBody, + Params: map[string]string{"sdkID": c.SdkID}, + }).Request() + if err != nil { + return nil, err + } + + var response *http.Response + response, err = requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return nil, err + } + + var responseBytes []byte + responseBytes, err = io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + var result create.SessionResult + err = json.Unmarshal(responseBytes, &result) + + return &result, err +} + +// GetSession retrieves the state of a previously created Yoti Doc Scan (IDV) session +func (c *Client) GetSession(sessionID string) (*retrieve.GetSessionResult, error) { + if sessionID == "" { + return nil, fmt.Errorf(mustNotBeEmptyString, "sessionID") + } + + request, err := (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodGet, + BaseURL: c.apiURL, + Endpoint: getSessionPath(sessionID), + Params: map[string]string{"sdkID": c.SdkID}, + }).Request() + if err != nil { + return nil, err + } + + var response *http.Response + response, err = requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return nil, err + } + + var responseBytes []byte + responseBytes, err = io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + var result retrieve.GetSessionResult + err = json.Unmarshal(responseBytes, &result) + + return &result, err +} + +// DeleteSession deletes a previously created Yoti Doc Scan (IDV) session and all of its related resources +func (c *Client) DeleteSession(sessionID string) error { + if sessionID == "" { + return fmt.Errorf(mustNotBeEmptyString, "sessionID") + } + + request, err := (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodDelete, + BaseURL: c.apiURL, + Endpoint: deleteSessionPath(sessionID), + Params: map[string]string{"sdkID": c.SdkID}, + }).Request() + if err != nil { + return err + } + + _, err = requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return err + } + + return nil +} + +// GetMediaContent retrieves media related to a Yoti Doc Scan (IDV) session based on the supplied media ID +func (c *Client) GetMediaContent(sessionID, mediaID string) (media.Media, error) { + if sessionID == "" { + return nil, fmt.Errorf(mustNotBeEmptyString, "sessionID") + } + + if mediaID == "" { + return nil, fmt.Errorf(mustNotBeEmptyString, "mediaID") + } + + request, err := (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodGet, + BaseURL: c.apiURL, + Endpoint: getMediaContentPath(sessionID, mediaID), + Params: map[string]string{"sdkID": c.SdkID}, + }).Request() + if err != nil { + return nil, err + } + + var response *http.Response + response, err = requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return nil, err + } + + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + var responseBytes []byte + responseBytes, err = io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + contentTypes := strings.Split(response.Header.Get("Content-type"), ";") + if len(contentTypes[0]) < 1 { + err = errors.New("unable to parse content type from response") + } + + result := media.NewMedia(contentTypes[0], responseBytes) + + return result, err +} + +// DeleteMediaContent deletes media related to a Yoti Doc Scan (IDV) session based on the supplied media ID +func (c *Client) DeleteMediaContent(sessionID, mediaID string) error { + if sessionID == "" { + return fmt.Errorf(mustNotBeEmptyString, "sessionID") + } + + if mediaID == "" { + return fmt.Errorf(mustNotBeEmptyString, "mediaID") + } + + request, err := (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodDelete, + BaseURL: c.apiURL, + Endpoint: deleteMediaPath(sessionID, mediaID), + Params: map[string]string{"sdkID": c.SdkID}, + }).Request() + if err != nil { + return err + } + + _, err = requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return err + } + + return nil +} + +// GetSupportedDocuments gets a slice of supported documents (defaults includeNonLatin to false) +func (c *Client) GetSupportedDocuments() (*supported.DocumentsResponse, error) { + return c.GetSupportedDocumentsWithNonLatin(false) +} + +// GetSupportedDocuments gets a slice of supported documents with bool param includeNonLatin +func (c *Client) GetSupportedDocumentsWithNonLatin(includeNonLatin bool) (*supported.DocumentsResponse, error) { + + request, err := (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodGet, + BaseURL: c.apiURL, + Endpoint: getSupportedDocumentsPath(), + Params: map[string]string{"includeNonLatin": strconv.FormatBool(includeNonLatin)}, + }).Request() + if err != nil { + return nil, err + } + + var response *http.Response + response, err = requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return nil, err + } + + var responseBytes []byte + responseBytes, err = io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + var result supported.DocumentsResponse + err = json.Unmarshal(responseBytes, &result) + + return &result, err +} + +// jsonMarshaler is a mockable JSON marshaler +type jsonMarshaler interface { + Marshal(v interface{}) ([]byte, error) +} + +func marshalJSON(jsonMarshaler jsonMarshaler, v interface{}) ([]byte, error) { + if jsonMarshaler != nil { + return jsonMarshaler.Marshal(v) + } + return json.Marshal(v) +} + +func (c *Client) CreateFaceCaptureResource(sessionID string, payload *facecapture.CreateFaceCaptureResourcePayload) (*retrieve.FaceCaptureResourceResponse, error) { + if sessionID == "" { + return nil, fmt.Errorf(mustNotBeEmptyString, "sessionID") + } + + body, err := marshalJSON(c.jsonMarshaler, payload) + if err != nil { + return nil, err + } + + request, err := (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodPost, + BaseURL: c.apiURL, + Endpoint: fmt.Sprintf("/sessions/%s/resources/face-capture", sessionID), + Params: map[string]string{"sdkID": c.SdkID}, + Headers: requests.JSONHeaders(), + Body: body, + }).Request() + if err != nil { + return nil, err + } + + resp, err := requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return nil, err + } + + var result retrieve.FaceCaptureResourceResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (c *Client) UploadFaceCaptureImage(sessionID, resourceID string, payload *facecapture.UploadFaceCaptureImagePayload) error { + if sessionID == "" || resourceID == "" { + return fmt.Errorf("sessionID and resourceID must not be empty") + } + + if err := payload.Prepare(); err != nil { + return fmt.Errorf("failed to prepare multipart payload: %w", err) + } + + request, err := (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodPut, + BaseURL: c.apiURL, + Endpoint: fmt.Sprintf("/sessions/%s/resources/face-capture/%s/image", sessionID, resourceID), + Params: map[string]string{"sdkID": c.SdkID}, + Body: payload.MultipartFormBody().Bytes(), + Headers: payload.Headers(), + }).Request() + + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + _, err = requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + return err +} + +func (c *Client) GetSessionConfiguration(sessionID string) (*retrieve.SessionConfigurationResponse, error) { + if sessionID == "" { + return nil, fmt.Errorf(mustNotBeEmptyString, "sessionID") + } + + request, err := (&requests.SignedRequest{ + Key: c.Key, + HTTPMethod: http.MethodGet, + BaseURL: c.apiURL, + Endpoint: fmt.Sprintf("/sessions/%s/configuration", sessionID), + Params: map[string]string{"sdkID": c.SdkID}, + }).Request() + if err != nil { + return nil, err + } + + response, err := requests.Execute(c.HTTPClient, request, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return nil, err + } + + var responseBytes []byte + responseBytes, err = io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + var result retrieve.SessionConfigurationResponse + + if err := json.Unmarshal(responseBytes, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &result, nil +} + +func (c *Client) AddFaceCaptureResourceToSession(sessionID string, base64Image string) error { + sessionConfig, err := c.GetSessionConfiguration(sessionID) + if err != nil { + return err + } + + if sessionConfig == nil { + return fmt.Errorf("sessionConfig is nil") + } + + capture := sessionConfig.GetCapture() + if capture == nil { + return fmt.Errorf("capture info is missing in sessionConfig") + } + + requirements := capture.GetFaceCaptureResourceRequirements() + if len(requirements) == 0 { + // No face capture resource requirement, nothing to add + return nil + } + + firstRequirement := requirements[0] + if firstRequirement == nil || firstRequirement.ID == "" { + return fmt.Errorf("invalid face capture resource requirement") + } + + payload := facecapture.NewCreateFaceCaptureResourcePayload(firstRequirement.ID) + + resource, err := c.CreateFaceCaptureResource(sessionID, payload) + if err != nil { + return err + } + + // Use the base64Image passed as a parameter + imageBytes, err := base64.StdEncoding.DecodeString(base64Image) + if err != nil { + return err + } + + imagePayload := facecapture.NewUploadFaceCaptureImagePayload("image/png", imageBytes) + return c.UploadFaceCaptureImage(sessionID, resource.ID, imagePayload) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/client_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/client_test.go new file mode 100644 index 0000000..1eb74e4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/client_test.go @@ -0,0 +1,868 @@ +package docscan + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/facecapture" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/retrieve" + "github.com/getyoti/yoti-go-sdk/v3/docscan/supported" + "github.com/getyoti/yoti-go-sdk/v3/media" + "gotest.tools/v3/assert" +) + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func TestClient_CreateSession(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + var clientSessionTokenTTL int = 100 + var clientSessionToken string = "8c91671a-7194-4ad7-8483-32703b965cfc" + var sessionID string = "c87c4f2a-13fd-4cc8-a0e4-f1637cf32f71" + + jsonResponse := fmt.Sprintf(`{"client_session_token_ttl":%d,"client_session_token":"%s","session_id":"%s"}`, clientSessionTokenTTL, clientSessionToken, sessionID) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusCreated, + Body: io.NopCloser(strings.NewReader(jsonResponse)), + }, nil + }, + } + + var sessionSpec *create.SessionSpecification + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + createSessionResult, err := client.CreateSession(sessionSpec) + assert.NilError(t, err) + + assert.Equal(t, clientSessionTokenTTL, createSessionResult.ClientSessionTokenTTL) + assert.Equal(t, clientSessionToken, createSessionResult.ClientSessionToken) + assert.Equal(t, sessionID, createSessionResult.SessionID) +} + +func TestClient_CreateSession_ShouldReturnJsonMarshalError(t *testing.T) { + client := Client{ + jsonMarshaler: &mockJSONMarshaler{ + marshal: func(v interface{}) ([]byte, error) { + return []byte{}, errors.New("some json error") + }, + }, + } + _, err := client.CreateSession(&create.SessionSpecification{}) + assert.ErrorContains(t, err, "some json error") +} + +func TestClient_CreateSession_ShouldReturnMissingKeyError(t *testing.T) { + client := Client{} + _, err := client.CreateSession(&create.SessionSpecification{}) + assert.ErrorContains(t, err, "missing private key") +} + +func TestClient_CreateSession_ShouldReturnResponseError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusBadRequest, + }, nil + }, + } + + var sessionSpec *create.SessionSpecification + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + _, err = client.CreateSession(sessionSpec) + assert.ErrorContains(t, err, "400: unknown HTTP error") +} + +func TestClient_GetSession(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + var clientSessionTokenTTL int = 100 + var clientSessionToken string = "8c91671a-7194-4ad7-8483-32703b965cfc" + var sessionID string = "c87c4f2a-13fd-4cc8-a0e4-f1637cf32f71" + var userTrackingID string = "user-tracking-id" + var state = "COMPLETED" + jsonResponse := fmt.Sprintf(`{"client_session_token_ttl":%d,"client_session_token":"%s","session_id":"%s","user_tracking_id":"%s","state":"%s"}`, clientSessionTokenTTL, clientSessionToken, sessionID, userTrackingID, state) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(jsonResponse)), + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + getSessionResult, err := client.GetSession(sessionID) + assert.NilError(t, err) + + assert.Equal(t, clientSessionTokenTTL, getSessionResult.ClientSessionTokenTTL) + assert.Equal(t, clientSessionToken, getSessionResult.ClientSessionToken) + assert.Equal(t, sessionID, getSessionResult.SessionID) + assert.Equal(t, userTrackingID, getSessionResult.UserTrackingID) + assert.Equal(t, state, getSessionResult.State) +} + +func TestClient_GetSession_ShouldReturnMissingKeyError(t *testing.T) { + client := Client{} + _, err := client.GetSession("some-id") + assert.ErrorContains(t, err, "missing private key") +} + +func TestClient_GetSession_ShouldReturnResponseError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusBadRequest, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + _, err = client.GetSession("some-id") + assert.ErrorContains(t, err, "400: unknown HTTP error") +} + +func TestClient_GetSession_ShouldReturnJsonError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("some-invalid-json")), + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + _, err = client.GetSession("some-id") + assert.ErrorContains(t, err, "invalid character") +} + +func TestClient_DeleteSession(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + err = client.DeleteSession("some-session-id") + assert.NilError(t, err) +} + +func TestClient_DeleteSession_ShouldReturnMissingKeyError(t *testing.T) { + client := Client{} + err := client.DeleteSession("some-id") + assert.ErrorContains(t, err, "missing private key") +} + +func TestClient_DeleteSession_ShouldReturnResponseError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusBadRequest, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + err = client.DeleteSession("some-id") + assert.ErrorContains(t, err, "400: unknown HTTP error") +} + +func TestClient_GetMediaContent(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + jpegImage := []byte("value") + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(jpegImage)), + Header: map[string][]string{"Content-Type": {media.ImageTypeJPEG}}, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + result, err := client.GetMediaContent("some-sessionID", "some-mediaID") + assert.NilError(t, err) + + assert.DeepEqual(t, jpegImage, result.Data()) + assert.Equal(t, media.ImageTypeJPEG, result.MIME()) +} + +func TestClient_GetMediaContent_NoContent(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusNoContent, + Header: map[string][]string{}, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + media, err := client.GetMediaContent("some-sessionID", "some-mediaID") + assert.Equal(t, media, nil) + assert.NilError(t, err) +} + +func TestClient_GetMediaContent_NoContentType(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + jpegImage := []byte("value") + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(jpegImage)), + Header: map[string][]string{}, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + _, err = client.GetMediaContent("some-sessionID", "some-mediaID") + assert.ErrorContains(t, err, "unable to parse content type from response") +} + +func TestClient_GetMediaContent_ShouldReturnMissingKeyError(t *testing.T) { + client := Client{} + _, err := client.GetMediaContent("some-id", "some-media-id") + assert.ErrorContains(t, err, "missing private key") +} + +func TestClient_GetMediaContent_ShouldReturnResponseError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusBadRequest, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + _, err = client.GetMediaContent("some-id", "some-media-id") + assert.ErrorContains(t, err, "400: unknown HTTP error") +} + +func TestClient_DeleteMediaContent(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + err = client.DeleteMediaContent("some-session-id", "some-media-id") + assert.NilError(t, err) +} + +func TestClient_DeleteMediaContent_ShouldReturnMissingKeyError(t *testing.T) { + client := Client{} + err := client.DeleteMediaContent("some-id", "some-media-id") + assert.ErrorContains(t, err, "missing private key") +} + +func TestClient_DeleteMediaContent_ShouldReturnResponseError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusBadRequest, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + err = client.DeleteMediaContent("some-id", "some-media-id") + assert.ErrorContains(t, err, "400: unknown HTTP error") +} + +func TestClient_GetSupportedDocuments(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + countryCodeUSA := "USA" + documentTypePassport := "PASSPORT" + isStrictlyLatin := true + documentsResponse := supported.DocumentsResponse{ + SupportedCountries: []*supported.Country{ + { + Code: countryCodeUSA, + SupportedDocuments: []*supported.Document{ + { + Type: documentTypePassport, + IsStrictlyLatin: isStrictlyLatin, + }, + }, + }, + }, + } + + jsonBytes, err := json.Marshal(documentsResponse) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(jsonBytes)), + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + result, err := client.GetSupportedDocuments() + assert.NilError(t, err) + + assert.Equal(t, result.SupportedCountries[0].Code, countryCodeUSA) + assert.Equal(t, result.SupportedCountries[0].SupportedDocuments[0].Type, documentTypePassport) + assert.Equal(t, result.SupportedCountries[0].SupportedDocuments[0].IsStrictlyLatin, isStrictlyLatin) +} + +func TestClient_GetSupportedDocuments_ShouldReturnMissingKeyError(t *testing.T) { + client := Client{} + _, err := client.GetSupportedDocuments() + assert.ErrorContains(t, err, "missing private key") +} + +func TestClient_GetSupportedDocuments_ShouldReturnResponseError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + HTTPClient := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusBadRequest, + }, nil + }, + } + + client := Client{ + SdkID: "sdkId", + Key: key, + HTTPClient: HTTPClient, + apiURL: "https://apiurl.com", + } + + _, err = client.GetSupportedDocuments() + assert.ErrorContains(t, err, "400: unknown HTTP error") +} + +func TestNewClient(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + assert.Equal(t, "sdkID", client.SdkID) +} + +func TestNewClient_EmptySdkID(t *testing.T) { + _, err := NewClient("", []byte("someKey")) + + assert.ErrorContains(t, err, "SdkID cannot be an empty string") +} + +func TestClient_GetSession_EmptySessionID(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + _, err = client.GetSession("") + assert.ErrorContains(t, err, "sessionID cannot be an empty string") +} + +func TestClient_DeleteSession_EmptySessionID(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + err = client.DeleteSession("") + assert.ErrorContains(t, err, "sessionID cannot be an empty string") +} + +func TestClient_GetMediaContent_EmptySessionID(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + _, err = client.GetMediaContent("", "someMediaID") + assert.ErrorContains(t, err, "sessionID cannot be an empty string") +} + +func TestClient_GetMediaContent_EmptyMediaID(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + _, err = client.GetMediaContent("someSessionID", "") + assert.ErrorContains(t, err, "mediaID cannot be an empty string") +} + +func TestClient_DeleteMediaContent_EmptySessionID(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + err = client.DeleteMediaContent("", "someMediaID") + assert.ErrorContains(t, err, "sessionID cannot be an empty string") +} + +func TestClient_DeleteMediaContent_EmptyMediaID(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + err = client.DeleteMediaContent("someSessionID", "") + assert.ErrorContains(t, err, "mediaID cannot be an empty string") +} + +func Test_EmptySdkID(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + _, err = client.GetSession("") + assert.ErrorContains(t, err, "sessionID cannot be an empty string") +} + +func TestNewClient_KeyLoad_Failure(t *testing.T) { + key, err := os.ReadFile("../test/test-key-invalid-format.pem") + assert.NilError(t, err) + + _, err = NewClient("sdkID", key) + + assert.ErrorContains(t, err, "invalid key: not PEM-encoded") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestClient_UsesDefaultApiUrl(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + assert.Equal(t, "https://api.yoti.com/idverify/v1", client.apiURL) +} + +func TestClient_UsesEnvVariable(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + os.Setenv("YOTI_DOC_SCAN_API_URL", "envBaseUrl") + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + assert.Equal(t, "envBaseUrl", client.apiURL) + + os.Unsetenv("YOTI_DOC_SCAN_API_URL") +} + +func TestClient_UsesOverrideApiUrlOverEnvVariable(t *testing.T) { + key, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + os.Setenv("YOTI_DOC_SCAN_API_URL", "envBaseUrl") + + client, err := NewClient("sdkID", key) + assert.NilError(t, err) + + client.OverrideAPIURL("overrideApiURL") + + assert.Equal(t, "overrideApiURL", client.apiURL) + + os.Unsetenv("YOTI_DOC_SCAN_API_URL") +} + +type mockJSONMarshaler struct { + marshal func(v interface{}) ([]byte, error) +} + +func (mock *mockJSONMarshaler) Marshal(v interface{}) ([]byte, error) { + if mock.marshal != nil { + return mock.marshal(v) + } + return nil, nil +} + +type testJSONMarshaler struct{} + +func (m testJSONMarshaler) Marshal(v interface{}) ([]byte, error) { + return json.Marshal(v) +} + +func TestClient_CreateFaceCaptureResource(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + + expected := &retrieve.FaceCaptureResourceResponse{ID: "resource-id"} + expectedBytes, _ := json.Marshal(expected) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader(expectedBytes)), + }, nil + }, + }, + apiURL: "https://example.com", + SdkID: "sdk-id", + jsonMarshaler: testJSONMarshaler{}, + } + + payload := facecapture.NewCreateFaceCaptureResourcePayload("requirement-id") + result, err := client.CreateFaceCaptureResource("session-id", payload) + + assert.NilError(t, err) + assert.Equal(t, result.ID, expected.ID) +} + +func TestClient_UploadFaceCaptureImage(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + + image := []byte("test-image") + payload := facecapture.NewUploadFaceCaptureImagePayload("image/png", image) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 204, + Body: io.NopCloser(bytes.NewReader([]byte{})), + }, nil + }, + }, + apiURL: "https://example.com", + SdkID: "sdk-id", + } + + err := client.UploadFaceCaptureImage("session-id", "resource-id", payload) + assert.NilError(t, err) +} + +func TestClient_GetSessionConfiguration(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + + expected := &retrieve.SessionConfigurationResponse{} + expectedBytes, _ := json.Marshal(expected) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader(expectedBytes)), + }, nil + }, + }, + apiURL: "https://example.com", + SdkID: "sdk-id", + } + + result, err := client.GetSessionConfiguration("session-id") + assert.NilError(t, err) + assert.DeepEqual(t, result, expected) +} + +func TestClient_GetSessionConfiguration_FailsOnEmptySessionID(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + client := Client{Key: key} + + _, err := client.GetSessionConfiguration("") + assert.Error(t, err, "sessionID cannot be an empty string") +} + +func TestClient_CreateFaceCaptureResource_FailsOnEmptySessionID(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + client := Client{Key: key} + + payload := facecapture.NewCreateFaceCaptureResourcePayload("requirement-id") + _, err := client.CreateFaceCaptureResource("", payload) + assert.Error(t, err, "sessionID cannot be an empty string") +} + +func TestClient_UploadFaceCaptureImage_FailsOnEmptyIDs(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + client := Client{Key: key} + + payload := facecapture.NewUploadFaceCaptureImagePayload("image/png", []byte("img")) + + err := client.UploadFaceCaptureImage("", "resource-id", payload) + assert.Error(t, err, "sessionID and resourceID must not be empty") + + err = client.UploadFaceCaptureImage("session-id", "", payload) + assert.Error(t, err, "sessionID and resourceID must not be empty") +} + +func TestClient_AddFaceCaptureResourceToSession_HappyPath(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + mockBase64Image := "aW1hZ2U=" // "image" + + mockConfigResponse := `{"capture":{"required_resources":[{"type":"FACE_CAPTURE","id":"requirement-id-123"}]}}` + mockResourceResponse := `{"id":"resource-id-456"}` + + var getConfigCalled, createResourceCalled, uploadImageCalled bool + + mockClient := &mockHTTPClient{ + do: func(req *http.Request) (*http.Response, error) { + if strings.Contains(req.URL.Path, "/configuration") { + getConfigCalled = true + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(mockConfigResponse))}, nil + } + if req.Method == http.MethodPost && strings.Contains(req.URL.Path, "/face-capture") { + createResourceCalled = true + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(mockResourceResponse))}, nil + } + if req.Method == http.MethodPut && strings.Contains(req.URL.Path, "/image") { + uploadImageCalled = true + return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte{}))}, nil + } + return nil, fmt.Errorf("unexpected request: %s %s", req.Method, req.URL.Path) + }, + } + + client := Client{ + Key: key, + HTTPClient: mockClient, + apiURL: "https://example.com", + SdkID: "sdk-id", + jsonMarshaler: testJSONMarshaler{}, + } + + err := client.AddFaceCaptureResourceToSession("session-id", mockBase64Image) + + assert.NilError(t, err) + assert.Assert(t, getConfigCalled, "GetSessionConfiguration should have been called") + assert.Assert(t, createResourceCalled, "CreateFaceCaptureResource should have been called") + assert.Assert(t, uploadImageCalled, "UploadFaceCaptureImage should have been called") +} + +func TestClient_AddFaceCaptureResourceToSession_GetConfigFails(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + expectedErr := errors.New("failed to get config") + + mockClient := &mockHTTPClient{ + do: func(req *http.Request) (*http.Response, error) { + return nil, expectedErr + }, + } + + client := Client{Key: key, HTTPClient: mockClient, apiURL: "https://example.com", SdkID: "sdk-id"} + err := client.AddFaceCaptureResourceToSession("session-id", "aW1hZ2U=") + + assert.ErrorIs(t, err, expectedErr) +} + +func TestClient_AddFaceCaptureResourceToSession_NoRequirements(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + mockConfigResponse := `{"capture":{"required_resources":[]}}` + + mockClient := &mockHTTPClient{ + do: func(req *http.Request) (*http.Response, error) { + if strings.Contains(req.URL.Path, "/configuration") { + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(mockConfigResponse))}, nil + } + t.Fatalf("unexpected request was made: %s %s", req.Method, req.URL.Path) + return nil, nil + }, + } + client := Client{Key: key, HTTPClient: mockClient, apiURL: "https://example.com", SdkID: "sdk-id"} + + err := client.AddFaceCaptureResourceToSession("session-id", "aW1hZ2U=") + assert.NilError(t, err) +} + +func TestClient_AddFaceCaptureResourceToSession_InvalidBase64(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 1024) + mockConfigResponse := `{"capture":{"required_resources":[{"type":"FACE_CAPTURE","id":"requirement-id-123"}]}}` + mockResourceResponse := `{"id":"resource-id-456"}` + + mockClient := &mockHTTPClient{ + do: func(req *http.Request) (*http.Response, error) { + if strings.Contains(req.URL.Path, "/configuration") { + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(mockConfigResponse))}, nil + } + if req.Method == http.MethodPost && strings.Contains(req.URL.Path, "/face-capture") { + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(mockResourceResponse))}, nil + } + t.Fatalf("unexpected request was made: %s %s", req.Method, req.URL.Path) + return nil, nil + }, + } + client := Client{ + Key: key, + HTTPClient: mockClient, + apiURL: "https://example.com", + SdkID: "sdk-id", + jsonMarshaler: testJSONMarshaler{}, + } + + err := client.AddFaceCaptureResourceToSession("session-id", "this is not base64") + assert.ErrorContains(t, err, "illegal base64 data") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/constants/constants.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/constants/constants.go new file mode 100644 index 0000000..5fbd0f7 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/constants/constants.go @@ -0,0 +1,45 @@ +package constants + +const ( + IDDocumentAuthenticity string = "ID_DOCUMENT_AUTHENTICITY" + IDDocumentComparison string = "ID_DOCUMENT_COMPARISON" + IDDocumentTextDataCheck string = "ID_DOCUMENT_TEXT_DATA_CHECK" + IDDocumentTextDataExtraction string = "ID_DOCUMENT_TEXT_DATA_EXTRACTION" + IDDocumentFaceMatch string = "ID_DOCUMENT_FACE_MATCH" + SupplementaryDocumentTextDataCheck string = "SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK" + ThirdPartyIdentityCheck string = "THIRD_PARTY_IDENTITY" + SupplementaryDocumentTextDataExtraction string = "SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION" + WatchlistScreening string = "WATCHLIST_SCREENING" + WatchlistAdvancedCA string = "WATCHLIST_ADVANCED_CA" + + WithYotiAccounts = "WITH_YOTI_ACCOUNT" + WithCustomAccount = "WITH_CUSTOM_ACCOUNT" + TypeList = "TYPE_LIST" + Profiles = "PROFILE" + Exact = "EXACT" + Fuzzy = "FUZZY" + + Liveness string = "LIVENESS" + Zoom string = "ZOOM" + Static string = "STATIC" + + Camera string = "CAMERA" + CameraAndUpload string = "CAMERA_AND_UPLOAD" + + ResourceUpdate string = "RESOURCE_UPDATE" + TaskCompletion string = "TASK_COMPLETION" + CheckCompletion string = "CHECK_COMPLETION" + SessionCompletion string = "SESSION_COMPLETION" + + Sanctions string = "SANCTIONS" + AdverseMedia string = "ADVERSE-MEDIA" + + Always string = "ALWAYS" + Fallback string = "FALLBACK" + Never string = "NEVER" + + ProofOfAddress string = "PROOF_OF_ADDRESS" + Early string = "EARLY" + JustInTime string = "JUST_IN_TIME" + FaceComparison string = "FACE_COMPARISON" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/endpoint.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/endpoint.go new file mode 100644 index 0000000..77e9d9c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/endpoint.go @@ -0,0 +1,27 @@ +package docscan + +import "fmt" + +func createSessionPath() string { + return "/sessions" +} + +func getSessionPath(sessionID string) string { + return fmt.Sprintf("/sessions/%s", sessionID) +} + +func deleteSessionPath(sessionID string) string { + return getSessionPath(sessionID) +} + +func getMediaContentPath(sessionID string, mediaID string) string { + return fmt.Sprintf("/sessions/%s/media/%s/content", sessionID, mediaID) +} + +func deleteMediaPath(sessionID string, mediaID string) string { + return getMediaContentPath(sessionID, mediaID) +} + +func getSupportedDocumentsPath() string { + return "/supported-documents" +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/client.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/client.go new file mode 100644 index 0000000..f86ecc9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/client.go @@ -0,0 +1,132 @@ +package sandbox + +import ( + "crypto/rsa" + "encoding/json" + "net/http" + "os" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request" + "github.com/getyoti/yoti-go-sdk/v3/requests" + yotirequest "github.com/getyoti/yoti-go-sdk/v3/requests" +) + +type jsonMarshaler interface { + Marshal(v interface{}) ([]byte, error) +} + +// Client is responsible for setting up test data in the sandbox instance. +type Client struct { + // SDK ID. This can be found in the Yoti Hub after you have created and activated an application. + SdkID string + // Private Key associated for your application, can be downloaded from the Yoti Hub. + Key *rsa.PrivateKey + // Mockable HTTP Client Interface + HTTPClient requests.HttpClient + // API URL to use. This is not required, and a default will be set if not provided. + apiURL string + // Mockable JSON marshaler + jsonMarshaler jsonMarshaler +} + +// NewClient constructs a Client object +func NewClient(sdkID string, key []byte) (*Client, error) { + decodedKey, err := cryptoutil.ParseRSAKey(key) + + if err != nil { + return nil, err + } + + return &Client{ + SdkID: sdkID, + Key: decodedKey, + }, err +} + +// OverrideAPIURL overrides the default API URL for this Yoti Client +func (client *Client) OverrideAPIURL(apiURL string) { + client.apiURL = apiURL +} + +func (client *Client) getAPIURL() string { + if client.apiURL == "" { + if value, exists := os.LookupEnv("YOTI_DOC_SCAN_API_URL"); exists && value != "" { + client.apiURL = value + } else { + client.apiURL = "https://api.yoti.com/sandbox/idverify/v1" + } + } + return client.apiURL +} + +func (client *Client) getHTTPClient() requests.HttpClient { + if client.HTTPClient != nil { + return client.HTTPClient + } + return http.DefaultClient +} + +func (client *Client) marshalJSON(v interface{}) ([]byte, error) { + if client.jsonMarshaler != nil { + return client.jsonMarshaler.Marshal(v) + } + return json.Marshal(v) +} + +func (client *Client) makeConfigureResponseRequest(request *http.Request) error { + _, err := requests.Execute(client.getHTTPClient(), request) + + if err != nil { + return err + } + + return nil +} + +// ConfigureSessionResponse configures the response for the session +func (client *Client) ConfigureSessionResponse(sessionID string, responseConfig *request.ResponseConfig) error { + requestEndpoint := "/sessions/" + sessionID + "/response-config" + requestBody, err := client.marshalJSON(responseConfig) + if err != nil { + return err + } + + signedRequest, err := (&yotirequest.SignedRequest{ + Key: client.Key, + HTTPMethod: http.MethodPut, + BaseURL: client.getAPIURL(), + Endpoint: requestEndpoint, + Headers: yotirequest.JSONHeaders(), + Body: requestBody, + Params: map[string]string{"sdkId": client.SdkID}, + }).Request() + if err != nil { + return err + } + + return client.makeConfigureResponseRequest(signedRequest) +} + +// ConfigureApplicationResponse configures the response for the application +func (client *Client) ConfigureApplicationResponse(responseConfig *request.ResponseConfig) error { + requestEndpoint := "/apps/" + client.SdkID + "/response-config" + requestBody, err := client.marshalJSON(responseConfig) + if err != nil { + return err + } + + signedRequest, err := (&yotirequest.SignedRequest{ + Key: client.Key, + HTTPMethod: http.MethodPut, + BaseURL: client.getAPIURL(), + Endpoint: requestEndpoint, + Headers: yotirequest.JSONHeaders(), + Body: requestBody, + }).Request() + if err != nil { + return err + } + + return client.makeConfigureResponseRequest(signedRequest) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/client_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/client_test.go new file mode 100644 index 0000000..a4876c2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/client_test.go @@ -0,0 +1,343 @@ +package sandbox + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "errors" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" + "gotest.tools/v3/assert" +) + +func TestClient_httpClient_ShouldReturnDefaultClient(t *testing.T) { + client := Client{} + assert.Check(t, client.getHTTPClient() != nil) +} + +func TestClient_ConfigureSessionResponse_ShouldReturnErrorIfNotCreated(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 400, + Body: io.NopCloser(strings.NewReader("")), + }, nil + }, + }, + } + err = client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.ErrorContains(t, err, "400: unknown HTTP error") +} + +func TestClient_ConfigureSessionResponse_ShouldReturnFormattedErrorWithResponse(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + jsonBytes, err := json.Marshal(yotierror.DataObject{ + Code: "SOME_CODE", + Message: "some message", + }) + assert.NilError(t, err) + + response := &http.Response{ + StatusCode: 400, + Body: io.NopCloser(bytes.NewReader(jsonBytes)), + } + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return response, nil + }, + }, + } + err = client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.ErrorContains(t, err, "400: SOME_CODE - some message") + + errorResponse := err.(*yotierror.Error).Response + assert.Equal(t, response, errorResponse) + + body, err := io.ReadAll(errorResponse.Body) + assert.NilError(t, err) + assert.Equal(t, string(body), string(jsonBytes)) +} + +func TestClient_ConfigureSessionResponse_ShouldReturnMissingKeyError(t *testing.T) { + client := Client{} + err := client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.ErrorContains(t, err, "missing private key") +} + +func TestClient_ConfigureSessionResponse_ShouldReturnJsonError(t *testing.T) { + client := Client{ + jsonMarshaler: &mockJSONMarshaler{ + marshal: func(v interface{}) ([]byte, error) { + return []byte{}, errors.New("some json error") + }, + }, + } + err := client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.ErrorContains(t, err, "some json error") +} + +func TestNewClient_ConfigureSessionResponse_Success(t *testing.T) { + key, err := os.ReadFile("../../test/test-key.pem") + assert.NilError(t, err) + + client, err := NewClient("ClientSDKID", key) + assert.NilError(t, err) + + client.HTTPClient = &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + }, nil + }, + } + + responseErr := client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.NilError(t, responseErr) +} + +func TestNewClient_KeyLoad_Failure(t *testing.T) { + key, err := os.ReadFile("../../test/test-key-invalid-format.pem") + assert.NilError(t, err) + + _, err = NewClient("", key) + + assert.ErrorContains(t, err, "invalid key: not PEM-encoded") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestClient_ConfigureSessionResponse_Success(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + }, nil + }, + }, + } + err = client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.NilError(t, err) +} + +func TestClient_ConfigureSessionResponse_ShouldReturnHttpClientError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{}, errors.New("some error") + }, + }, + } + err = client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.ErrorContains(t, err, "some error") +} + +func TestClient_ConfigureApplicationResponse_ShouldReturnErrorIfNotCreated(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 401, + Body: io.NopCloser(strings.NewReader("")), + }, nil + }, + }, + } + err = client.ConfigureApplicationResponse(&request.ResponseConfig{}) + assert.ErrorContains(t, err, "401: unknown HTTP error") +} + +func TestClient_ConfigureApplicationResponse_ShouldReturnMissingKeyError(t *testing.T) { + client := Client{} + err := client.ConfigureApplicationResponse(&request.ResponseConfig{}) + assert.ErrorContains(t, err, "missing private key") +} + +func TestClient_ConfigureApplicationResponse_ShouldReturnJsonError(t *testing.T) { + client := Client{ + jsonMarshaler: &mockJSONMarshaler{ + marshal: func(v interface{}) ([]byte, error) { + return []byte{}, errors.New("some json error") + }, + }, + } + err := client.ConfigureApplicationResponse(&request.ResponseConfig{}) + assert.ErrorContains(t, err, "some json error") +} + +func TestClient_ConfigureApplicationResponse_Success(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + }, nil + }, + }, + } + err = client.ConfigureApplicationResponse(&request.ResponseConfig{}) + assert.NilError(t, err) +} + +func TestClient_ConfigureApplicationResponse_ShouldReturnHttpClientError(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + client := Client{ + Key: key, + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{}, errors.New("some error") + }, + }, + } + err = client.ConfigureApplicationResponse(&request.ResponseConfig{}) + assert.ErrorContains(t, err, "some error") +} + +func TestClient_ConfigureSessionResponseUsesConstructorApiUrlOverEnvVariable(t *testing.T) { + client := createSandboxClient(t, "constuctorApiURL") + os.Setenv("YOTI_DOC_SCAN_API_URL", "envBaseUrl") + + err := client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.NilError(t, err) + + assert.Equal(t, "constuctorApiURL", client.getAPIURL()) +} + +func TestClient_ConfigureSessionResponseUsesOverrideApiUrlOverEnvVariable(t *testing.T) { + client := createSandboxClient(t, "") + client.OverrideAPIURL("overrideApiURL") + os.Setenv("YOTI_DOC_SCAN_API_URL", "envBaseUrl") + + err := client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.NilError(t, err) + + assert.Equal(t, "overrideApiURL", client.getAPIURL()) +} + +func TestClient_ConfigureSessionResponseUsesEnvVariable(t *testing.T) { + client := createSandboxClient(t, "") + + os.Setenv("YOTI_DOC_SCAN_API_URL", "envApiURL") + + err := client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.NilError(t, err) + + assert.Equal(t, "envApiURL", client.getAPIURL()) +} + +func TestClient_ConfigureSessionResponseUsesDefaultUrlAsFallbackWithEmptyEnvValue(t *testing.T) { + os.Setenv("YOTI_DOC_SCAN_API_URL", "") + + client := createSandboxClient(t, "") + + err := client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.NilError(t, err) + + assert.Equal(t, "https://api.yoti.com/sandbox/idverify/v1", client.getAPIURL()) +} + +func TestClient_ConfigureSessionResponseUsesDefaultUrlAsFallbackWithNoEnvValue(t *testing.T) { + os.Unsetenv("YOTI_DOC_SCAN_API_URL") + + client := createSandboxClient(t, "") + + err := client.ConfigureSessionResponse("some_session_id", &request.ResponseConfig{}) + assert.NilError(t, err) + + assert.Equal(t, "https://api.yoti.com/sandbox/idverify/v1", client.getAPIURL()) +} + +func createSandboxClient(t *testing.T, constructorApiURL string) (client Client) { + keyBytes, fileErr := os.ReadFile("../../test/test-key.pem") + assert.NilError(t, fileErr) + + pemFile, parseErr := cryptoutil.ParseRSAKey(keyBytes) + assert.NilError(t, parseErr) + + if constructorApiURL == "" { + return Client{ + Key: pemFile, + SdkID: "ClientSDKID", + HTTPClient: mockHTTPClientCreatedResponse(), + } + } + + return Client{ + Key: pemFile, + SdkID: "ClientSDKID", + HTTPClient: mockHTTPClientCreatedResponse(), + apiURL: constructorApiURL, + } + +} + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func mockHTTPClientCreatedResponse() *mockHTTPClient { + return &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + }, nil + }, + } +} + +type mockJSONMarshaler struct { + marshal func(v interface{}) ([]byte, error) +} + +func (mock *mockJSONMarshaler) Marshal(v interface{}) ([]byte, error) { + if mock.marshal != nil { + return mock.marshal(v) + } + return nil, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/check.go new file mode 100644 index 0000000..e2f2a22 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/check.go @@ -0,0 +1,42 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" +) + +type check struct { + Result checkResult `json:"result"` +} + +type checkBuilder struct { + recommendation *report.Recommendation + breakdowns []*report.Breakdown +} + +type checkResult struct { + Report checkReport `json:"report"` +} + +type checkReport struct { + Recommendation *report.Recommendation `json:"recommendation,omitempty"` + Breakdown []*report.Breakdown `json:"breakdown,omitempty"` +} + +func (b *checkBuilder) withRecommendation(recommendation *report.Recommendation) { + b.recommendation = recommendation +} + +func (b *checkBuilder) withBreakdown(breakdown *report.Breakdown) { + b.breakdowns = append(b.breakdowns, breakdown) +} + +func (b *checkBuilder) build() *check { + return &check{ + Result: checkResult{ + Report: checkReport{ + Recommendation: b.recommendation, + Breakdown: b.breakdowns, + }, + }, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_authenticity_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_authenticity_check.go new file mode 100644 index 0000000..587aabf --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_authenticity_check.go @@ -0,0 +1,46 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +// DocumentAuthenticityCheck Represents a document authenticity check +type DocumentAuthenticityCheck struct { + *documentCheck +} + +// DocumentAuthenticityCheckBuilder builds a DocumentAuthenticityCheck +type DocumentAuthenticityCheckBuilder struct { + documentCheckBuilder +} + +// NewDocumentAuthenticityCheckBuilder creates a new DocumentAuthenticityCheckBuilder +func NewDocumentAuthenticityCheckBuilder() *DocumentAuthenticityCheckBuilder { + return &DocumentAuthenticityCheckBuilder{} +} + +// WithRecommendation sets the recommendation on the check +func (b *DocumentAuthenticityCheckBuilder) WithRecommendation(recommendation *report.Recommendation) *DocumentAuthenticityCheckBuilder { + b.documentCheckBuilder.withRecommendation(recommendation) + return b +} + +// WithBreakdown adds a breakdown item to the check +func (b *DocumentAuthenticityCheckBuilder) WithBreakdown(breakdown *report.Breakdown) *DocumentAuthenticityCheckBuilder { + b.documentCheckBuilder.withBreakdown(breakdown) + return b +} + +// WithDocumentFilter adds a document filter to the check +func (b *DocumentAuthenticityCheckBuilder) WithDocumentFilter(filter *filter.DocumentFilter) *DocumentAuthenticityCheckBuilder { + b.documentCheckBuilder.withDocumentFilter(filter) + return b +} + +// Build creates a new DocumentAuthenticityCheck +func (b *DocumentAuthenticityCheckBuilder) Build() (*DocumentAuthenticityCheck, error) { + return &DocumentAuthenticityCheck{ + documentCheck: b.documentCheckBuilder.build(), + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_authenticity_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_authenticity_check_test.go new file mode 100644 index 0000000..0610421 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_authenticity_check_test.go @@ -0,0 +1,53 @@ +package check + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +func ExampleDocumentAuthenticityCheckBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + docFilter, err := filter.NewDocumentFilterBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + check, err := NewDocumentAuthenticityCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + WithDocumentFilter(docFilter). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}},"document_filter":{"document_types":[],"country_codes":[]}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_check.go new file mode 100644 index 0000000..42caadc --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_check.go @@ -0,0 +1,26 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +type documentCheck struct { + *check + DocumentFilter *filter.DocumentFilter `json:"document_filter,omitempty"` +} + +type documentCheckBuilder struct { + checkBuilder + documentFilter *filter.DocumentFilter +} + +func (b *documentCheckBuilder) withDocumentFilter(filter *filter.DocumentFilter) { + b.documentFilter = filter +} + +func (b *documentCheckBuilder) build() *documentCheck { + return &documentCheck{ + check: b.checkBuilder.build(), + DocumentFilter: b.documentFilter, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_face_match_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_face_match_check.go new file mode 100644 index 0000000..5c3c1b2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_face_match_check.go @@ -0,0 +1,46 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +// DocumentFaceMatchCheck represents a face match check +type DocumentFaceMatchCheck struct { + *documentCheck +} + +// DocumentFaceMatchCheckBuilder builds a DocumentFaceMatchCheck +type DocumentFaceMatchCheckBuilder struct { + documentCheckBuilder +} + +// NewDocumentFaceMatchCheckBuilder creates a new DocumentFaceMatchCheckBuilder +func NewDocumentFaceMatchCheckBuilder() *DocumentFaceMatchCheckBuilder { + return &DocumentFaceMatchCheckBuilder{} +} + +// WithRecommendation sets the recommendation on the check +func (b *DocumentFaceMatchCheckBuilder) WithRecommendation(recommendation *report.Recommendation) *DocumentFaceMatchCheckBuilder { + b.documentCheckBuilder.withRecommendation(recommendation) + return b +} + +// WithBreakdown adds a breakdown item to the check +func (b *DocumentFaceMatchCheckBuilder) WithBreakdown(breakdown *report.Breakdown) *DocumentFaceMatchCheckBuilder { + b.documentCheckBuilder.withBreakdown(breakdown) + return b +} + +// WithDocumentFilter adds a document filter to the check +func (b *DocumentFaceMatchCheckBuilder) WithDocumentFilter(filter *filter.DocumentFilter) *DocumentFaceMatchCheckBuilder { + b.documentCheckBuilder.withDocumentFilter(filter) + return b +} + +// Build creates a new DocumentFaceMatchCheck +func (b *DocumentFaceMatchCheckBuilder) Build() (*DocumentFaceMatchCheck, error) { + return &DocumentFaceMatchCheck{ + documentCheck: b.documentCheckBuilder.build(), + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_face_match_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_face_match_check_test.go new file mode 100644 index 0000000..32005f8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_face_match_check_test.go @@ -0,0 +1,53 @@ +package check + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +func ExampleDocumentFaceMatchCheckBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + docFilter, err := filter.NewDocumentFilterBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + check, err := NewDocumentFaceMatchCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + WithDocumentFilter(docFilter). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}},"document_filter":{"document_types":[],"country_codes":[]}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_text_data_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_text_data_check.go new file mode 100644 index 0000000..88aadcd --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_text_data_check.go @@ -0,0 +1,75 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +// DocumentTextDataCheck represents a document text data check +type DocumentTextDataCheck struct { + Result DocumentTextDataCheckResult `json:"result"` + *documentCheck +} + +// DocumentTextDataCheckBuilder builds a DocumentTextDataCheck +type DocumentTextDataCheckBuilder struct { + documentCheckBuilder + documentFields map[string]interface{} +} + +// DocumentTextDataCheckResult represent a document text data check result +type DocumentTextDataCheckResult struct { + checkResult + DocumentFields map[string]interface{} `json:"document_fields,omitempty"` +} + +// NewDocumentTextDataCheckBuilder builds a new DocumentTextDataCheckResult +func NewDocumentTextDataCheckBuilder() *DocumentTextDataCheckBuilder { + return &DocumentTextDataCheckBuilder{} +} + +// WithRecommendation sets the recommendation on the check +func (b *DocumentTextDataCheckBuilder) WithRecommendation(recommendation *report.Recommendation) *DocumentTextDataCheckBuilder { + b.documentCheckBuilder.withRecommendation(recommendation) + return b +} + +// WithBreakdown adds a breakdown item to the check +func (b *DocumentTextDataCheckBuilder) WithBreakdown(breakdown *report.Breakdown) *DocumentTextDataCheckBuilder { + b.documentCheckBuilder.withBreakdown(breakdown) + return b +} + +// WithDocumentFilter adds a document filter to the check +func (b *DocumentTextDataCheckBuilder) WithDocumentFilter(filter *filter.DocumentFilter) *DocumentTextDataCheckBuilder { + b.documentCheckBuilder.withDocumentFilter(filter) + return b +} + +// WithDocumentField adds a document field to the text data check +func (b *DocumentTextDataCheckBuilder) WithDocumentField(key string, value interface{}) *DocumentTextDataCheckBuilder { + if b.documentFields == nil { + b.documentFields = make(map[string]interface{}) + } + b.documentFields[key] = value + return b +} + +// WithDocumentFields sets document fields +func (b *DocumentTextDataCheckBuilder) WithDocumentFields(documentFields map[string]interface{}) *DocumentTextDataCheckBuilder { + b.documentFields = documentFields + return b +} + +// Build creates a new DocumentTextDataCheck +func (b *DocumentTextDataCheckBuilder) Build() (*DocumentTextDataCheck, error) { + docCheck := b.documentCheckBuilder.build() + + return &DocumentTextDataCheck{ + documentCheck: docCheck, + Result: DocumentTextDataCheckResult{ + checkResult: docCheck.Result, + DocumentFields: b.documentFields, + }, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_text_data_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_text_data_check_test.go new file mode 100644 index 0000000..1924d85 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/document_text_data_check_test.go @@ -0,0 +1,98 @@ +package check + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +func ExampleDocumentTextDataCheckBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + docFilter, err := filter.NewDocumentFilterBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + check, err := NewDocumentTextDataCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + WithDocumentFilter(docFilter). + WithDocumentField("some-key", "some-value"). + WithDocumentField("some-other-key", map[string]string{ + "some-nested-key": "some-nested-value", + }). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]},"document_fields":{"some-key":"some-value","some-other-key":{"some-nested-key":"some-nested-value"}}},"document_filter":{"document_types":[],"country_codes":[]}} +} + +func ExampleDocumentTextDataCheckBuilder_WithDocumentFields() { + check, err := NewDocumentTextDataCheckBuilder(). + WithDocumentFields(map[string]interface{}{ + "some-key": "some-value", + "some-other-key": map[string]string{ + "some-nested-key": "some-nested-value", + }, + }). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{},"document_fields":{"some-key":"some-value","some-other-key":{"some-nested-key":"some-nested-value"}}}} +} + +func ExampleDocumentTextDataCheckBuilder_minimal() { + check, err := NewDocumentTextDataCheckBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{}}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/id_document_comparison_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/id_document_comparison_check.go new file mode 100644 index 0000000..1a122d1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/id_document_comparison_check.go @@ -0,0 +1,49 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +// IDDocumentComparisonCheck Represents a document authenticity check +type IDDocumentComparisonCheck struct { + *check + SecondaryDocumentFilter *filter.DocumentFilter `json:"secondary_document_filter,omitempty"` +} + +// IDDocumentComparisonCheckBuilder builds a IDDocumentComparisonCheck +type IDDocumentComparisonCheckBuilder struct { + checkBuilder + secondaryDocumentFilter *filter.DocumentFilter +} + +// NewIDDocumentComparisonCheckBuilder creates a new IDDocumentComparisonCheckBuilder +func NewIDDocumentComparisonCheckBuilder() *IDDocumentComparisonCheckBuilder { + return &IDDocumentComparisonCheckBuilder{} +} + +// WithRecommendation sets the recommendation on the check +func (b *IDDocumentComparisonCheckBuilder) WithRecommendation(recommendation *report.Recommendation) *IDDocumentComparisonCheckBuilder { + b.checkBuilder.withRecommendation(recommendation) + return b +} + +// WithBreakdown adds a breakdown item to the check +func (b *IDDocumentComparisonCheckBuilder) WithBreakdown(breakdown *report.Breakdown) *IDDocumentComparisonCheckBuilder { + b.checkBuilder.withBreakdown(breakdown) + return b +} + +// WithSecondaryDocumentFilter adds a secondary document filter to the check +func (b *IDDocumentComparisonCheckBuilder) WithSecondaryDocumentFilter(filter *filter.DocumentFilter) *IDDocumentComparisonCheckBuilder { + b.secondaryDocumentFilter = filter + return b +} + +// Build creates a new IDDocumentComparisonCheck +func (b *IDDocumentComparisonCheckBuilder) Build() (*IDDocumentComparisonCheck, error) { + return &IDDocumentComparisonCheck{ + check: b.checkBuilder.build(), + SecondaryDocumentFilter: b.secondaryDocumentFilter, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/id_document_comparison_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/id_document_comparison_check_test.go new file mode 100644 index 0000000..ed9af75 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/id_document_comparison_check_test.go @@ -0,0 +1,73 @@ +package check + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +func ExampleIDDocumentComparisonCheckBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + WithDetail("some_name", "some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + docFilter, err := filter.NewDocumentFilterBuilder(). + WithCountryCode("AUS"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + idDocumentComparisonCheck, err := NewIDDocumentComparisonCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + WithSecondaryDocumentFilter(docFilter). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(idDocumentComparisonCheck) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[{"name":"some_name","value":"some_value"}]}]}},"secondary_document_filter":{"document_types":[],"country_codes":["AUS"]}} +} + +func ExampleIDDocumentComparisonCheckBuilder_minimal() { + idDocumentComparisonCheck, err := NewIDDocumentComparisonCheckBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(idDocumentComparisonCheck) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{}}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/liveness_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/liveness_check.go new file mode 100644 index 0000000..21f9e4e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/liveness_check.go @@ -0,0 +1,24 @@ +package check + +// LivenessCheck represents a liveness check +type LivenessCheck struct { + *check + LivenessType string `json:"liveness_type"` +} + +type livenessCheckBuilder struct { + checkBuilder + livenessType string +} + +func (b *livenessCheckBuilder) withLivenessType(livenessType string) *livenessCheckBuilder { + b.livenessType = livenessType + return b +} + +func (b *livenessCheckBuilder) build() *LivenessCheck { + return &LivenessCheck{ + LivenessType: b.livenessType, + check: b.checkBuilder.build(), + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/breakdown.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/breakdown.go new file mode 100644 index 0000000..a681417 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/breakdown.go @@ -0,0 +1,72 @@ +package report + +import ( + "errors" +) + +// Breakdown describes a breakdown on check +type Breakdown struct { + SubCheck string `json:"sub_check"` + Result string `json:"result"` + Details []*detail `json:"details"` +} + +// BreakdownBuilder builds a Breakdown +type BreakdownBuilder struct { + subCheck string + result string + details []*detail +} + +// Detail is an individual breakdown detail +type detail struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// NewBreakdownBuilder creates a new BreakdownBuilder +func NewBreakdownBuilder() *BreakdownBuilder { + return &BreakdownBuilder{ + details: []*detail{}, + } +} + +// WithSubCheck sets the sub check of a Breakdown +func (b *BreakdownBuilder) WithSubCheck(subCheck string) *BreakdownBuilder { + b.subCheck = subCheck + return b +} + +// WithResult sets the result of a Breakdown +func (b *BreakdownBuilder) WithResult(result string) *BreakdownBuilder { + b.result = result + return b +} + +// WithDetail sets the Detail of a Breakdown +func (b *BreakdownBuilder) WithDetail(name string, value string) *BreakdownBuilder { + b.details = append(b.details, &detail{ + Name: name, + Value: value, + }) + return b +} + +// Build creates a new Breakdown +func (b *BreakdownBuilder) Build() (*Breakdown, error) { + breakdown := &Breakdown{ + SubCheck: b.subCheck, + Result: b.result, + Details: b.details, + } + + if len(breakdown.SubCheck) == 0 { + return nil, errors.New("Sub Check cannot be empty") + } + + if len(breakdown.Result) == 0 { + return nil, errors.New("Result cannot be empty") + } + + return breakdown, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/breakdown_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/breakdown_test.go new file mode 100644 index 0000000..98af0f0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/breakdown_test.go @@ -0,0 +1,80 @@ +package report + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func Test_BreakdownBuilder(t *testing.T) { + breakdown, err := NewBreakdownBuilder(). + WithSubCheck("some_sub_check"). + WithResult("some_result"). + WithDetail("some_name", "some_value"). + Build() + + assert.NilError(t, err) + assert.Equal(t, breakdown.SubCheck, "some_sub_check") + assert.Equal(t, breakdown.Result, "some_result") + assert.Equal(t, breakdown.Details[0].Name, "some_name") + assert.Equal(t, breakdown.Details[0].Value, "some_value") +} + +func Test_BreakdownBuilder_ShouldRequireSubCheck(t *testing.T) { + _, err := NewBreakdownBuilder(). + WithResult("some_result"). + Build() + + assert.Error(t, err, "Sub Check cannot be empty") +} + +func Test_BreakdownBuilder_ShouldRequireResult(t *testing.T) { + _, err := NewBreakdownBuilder(). + WithSubCheck("some_sub_check"). + Build() + + assert.Error(t, err, "Result cannot be empty") +} + +func ExampleBreakdownBuilder() { + breakdown, err := NewBreakdownBuilder(). + WithSubCheck("some_sub_check"). + WithResult("some_result"). + WithDetail("some_name", "some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(breakdown) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"sub_check":"some_sub_check","result":"some_result","details":[{"name":"some_name","value":"some_value"}]} +} + +func ExampleBreakdownBuilder_minimal() { + breakdown, err := NewBreakdownBuilder(). + WithSubCheck("some_sub_check"). + WithResult("some_result"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(breakdown) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"sub_check":"some_sub_check","result":"some_result","details":[]} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/recommendation.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/recommendation.go new file mode 100644 index 0000000..b06e6fe --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/recommendation.go @@ -0,0 +1,57 @@ +package report + +import ( + "errors" +) + +// Recommendation describes a recommendation on check +type Recommendation struct { + Value string `json:"value"` + Reason string `json:"reason,omitempty"` + RecoverySuggestion string `json:"recovery_suggestion,omitempty"` +} + +// RecommendationBuilder builds a Recommendation +type RecommendationBuilder struct { + value string + reason string + recoverySuggestion string +} + +// NewRecommendationBuilder creates a new RecommendationBuilder +func NewRecommendationBuilder() *RecommendationBuilder { + return &RecommendationBuilder{} +} + +// WithReason sets the reason of a Recommendation +func (b *RecommendationBuilder) WithReason(reason string) *RecommendationBuilder { + b.reason = reason + return b +} + +// WithValue sets the value of a Recommendation +func (b *RecommendationBuilder) WithValue(value string) *RecommendationBuilder { + b.value = value + return b +} + +// WithRecoverySuggestion sets the recovery suggestion of a Recommendation +func (b *RecommendationBuilder) WithRecoverySuggestion(recoverySuggestion string) *RecommendationBuilder { + b.recoverySuggestion = recoverySuggestion + return b +} + +// Build creates a new Recommendation +func (b *RecommendationBuilder) Build() (*Recommendation, error) { + recommendation := &Recommendation{ + Value: b.value, + Reason: b.reason, + RecoverySuggestion: b.recoverySuggestion, + } + + if len(recommendation.Value) == 0 { + return nil, errors.New("Value cannot be empty") + } + + return recommendation, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/recommendation_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/recommendation_test.go new file mode 100644 index 0000000..03a7ecf --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/report/recommendation_test.go @@ -0,0 +1,68 @@ +package report + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func TestRecommendationBuilder(t *testing.T) { + recommendation, err := NewRecommendationBuilder(). + WithValue("some_value"). + WithReason("some_reason"). + WithRecoverySuggestion("some_suggestion"). + Build() + + assert.NilError(t, err) + assert.Equal(t, recommendation.Reason, "some_reason") + assert.Equal(t, recommendation.Value, "some_value") + assert.Equal(t, recommendation.RecoverySuggestion, "some_suggestion") +} + +func TestRecommendationBuilder_ShouldRequireValue(t *testing.T) { + _, err := NewRecommendationBuilder().Build() + + assert.Error(t, err, "Value cannot be empty") +} + +func ExampleRecommendationBuilder() { + recommendation, err := NewRecommendationBuilder(). + WithReason("some_reason"). + WithValue("some_value"). + WithRecoverySuggestion("some_suggestion"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(recommendation) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"some_value","reason":"some_reason","recovery_suggestion":"some_suggestion"} +} + +func ExampleRecommendationBuilder_minimal() { + recommendation, err := NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(recommendation) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"some_value"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/static_liveness_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/static_liveness_check.go new file mode 100644 index 0000000..99178ea --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/static_liveness_check.go @@ -0,0 +1,37 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" +) + +// StaticLivenessCheckBuilder builds a Static LivenessCheck +type StaticLivenessCheckBuilder struct { + livenessCheckBuilder +} + +// NewStaticLivenessCheckBuilder creates a new StaticLivenessCheckBuilder +func NewStaticLivenessCheckBuilder() *StaticLivenessCheckBuilder { + return &StaticLivenessCheckBuilder{} +} + +// WithRecommendation sets the recommendation on the check +func (b *StaticLivenessCheckBuilder) WithRecommendation(recommendation *report.Recommendation) *StaticLivenessCheckBuilder { + b.livenessCheckBuilder.withRecommendation(recommendation) + return b +} + +// WithBreakdown adds a breakdown item to the check +func (b *StaticLivenessCheckBuilder) WithBreakdown(breakdown *report.Breakdown) *StaticLivenessCheckBuilder { + b.livenessCheckBuilder.withBreakdown(breakdown) + return b +} + +// Build creates a new LivenessCheck +func (b *StaticLivenessCheckBuilder) Build() (*LivenessCheck, error) { + livenessCheck := b.livenessCheckBuilder. + withLivenessType(constants.Static). + build() + + return livenessCheck, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/static_liveness_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/static_liveness_check_test.go new file mode 100644 index 0000000..d15113c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/static_liveness_check_test.go @@ -0,0 +1,45 @@ +package check + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" +) + +func ExampleStaticLivenessCheckBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + check, err := NewStaticLivenessCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}},"liveness_type":"STATIC"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/supplementary_document_text_data_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/supplementary_document_text_data_check.go new file mode 100644 index 0000000..80ca379 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/supplementary_document_text_data_check.go @@ -0,0 +1,75 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +// SupplementaryDocumentTextDataCheck represents a supplementary document text data check +type SupplementaryDocumentTextDataCheck struct { + Result SupplementaryDocumentTextDataCheckResult `json:"result"` + *documentCheck +} + +// SupplementaryDocumentTextDataCheckBuilder builds a SupplementaryDocumentTextDataCheck +type SupplementaryDocumentTextDataCheckBuilder struct { + documentCheckBuilder + documentFields map[string]interface{} +} + +// SupplementaryDocumentTextDataCheckResult represents a document text data check result +type SupplementaryDocumentTextDataCheckResult struct { + checkResult + DocumentFields map[string]interface{} `json:"document_fields,omitempty"` +} + +// NewSupplementaryDocumentTextDataCheckBuilder builds a new SupplementaryDocumentTextDataCheckResult +func NewSupplementaryDocumentTextDataCheckBuilder() *SupplementaryDocumentTextDataCheckBuilder { + return &SupplementaryDocumentTextDataCheckBuilder{} +} + +// WithRecommendation sets the recommendation on the check +func (b *SupplementaryDocumentTextDataCheckBuilder) WithRecommendation(recommendation *report.Recommendation) *SupplementaryDocumentTextDataCheckBuilder { + b.documentCheckBuilder.withRecommendation(recommendation) + return b +} + +// WithBreakdown adds a breakdown item to the check +func (b *SupplementaryDocumentTextDataCheckBuilder) WithBreakdown(breakdown *report.Breakdown) *SupplementaryDocumentTextDataCheckBuilder { + b.documentCheckBuilder.withBreakdown(breakdown) + return b +} + +// WithDocumentFilter adds a document filter to the check +func (b *SupplementaryDocumentTextDataCheckBuilder) WithDocumentFilter(filter *filter.DocumentFilter) *SupplementaryDocumentTextDataCheckBuilder { + b.documentCheckBuilder.withDocumentFilter(filter) + return b +} + +// WithDocumentField adds a document field to the text data check +func (b *SupplementaryDocumentTextDataCheckBuilder) WithDocumentField(key string, value interface{}) *SupplementaryDocumentTextDataCheckBuilder { + if b.documentFields == nil { + b.documentFields = make(map[string]interface{}) + } + b.documentFields[key] = value + return b +} + +// WithDocumentFields sets document fields +func (b *SupplementaryDocumentTextDataCheckBuilder) WithDocumentFields(documentFields map[string]interface{}) *SupplementaryDocumentTextDataCheckBuilder { + b.documentFields = documentFields + return b +} + +// Build creates a new SupplementaryDocumentTextDataCheck +func (b *SupplementaryDocumentTextDataCheckBuilder) Build() (*SupplementaryDocumentTextDataCheck, error) { + docCheck := b.documentCheckBuilder.build() + + return &SupplementaryDocumentTextDataCheck{ + documentCheck: docCheck, + Result: SupplementaryDocumentTextDataCheckResult{ + checkResult: docCheck.Result, + DocumentFields: b.documentFields, + }, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/supplementary_document_text_data_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/supplementary_document_text_data_check_test.go new file mode 100644 index 0000000..630ab54 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/supplementary_document_text_data_check_test.go @@ -0,0 +1,98 @@ +package check + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +func ExampleSupplementaryDocumentTextDataCheckBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + docFilter, err := filter.NewDocumentFilterBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + check, err := NewSupplementaryDocumentTextDataCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + WithDocumentFilter(docFilter). + WithDocumentField("some-key", "some-value"). + WithDocumentField("some-other-key", map[string]string{ + "some-nested-key": "some-nested-value", + }). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]},"document_fields":{"some-key":"some-value","some-other-key":{"some-nested-key":"some-nested-value"}}},"document_filter":{"document_types":[],"country_codes":[]}} +} + +func ExampleSupplementaryDocumentTextDataCheckBuilder_WithDocumentFields() { + check, err := NewSupplementaryDocumentTextDataCheckBuilder(). + WithDocumentFields(map[string]interface{}{ + "some-key": "some-value", + "some-other-key": map[string]string{ + "some-nested-key": "some-nested-value", + }, + }). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{},"document_fields":{"some-key":"some-value","some-other-key":{"some-nested-key":"some-nested-value"}}}} +} + +func ExampleSupplementaryDocumentTextDataCheckBuilder_minimal() { + check, err := NewSupplementaryDocumentTextDataCheckBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{}}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/third_party_identity_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/third_party_identity_check.go new file mode 100644 index 0000000..d5e01da --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/third_party_identity_check.go @@ -0,0 +1,37 @@ +package check + +import "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + +// ThirdPartyIdentityCheck defines a sandbox check with a third party credit reporting agency +type ThirdPartyIdentityCheck struct { + *check +} + +// ThirdPartyIdentityCheckBuilder builds a ThirdPartyIdentityCheck +type ThirdPartyIdentityCheckBuilder struct { + checkBuilder +} + +// NewThirdPartyIdentityCheckBuilder creates a new ThirdPartyIdentityCheckBuilder +func NewThirdPartyIdentityCheckBuilder() *ThirdPartyIdentityCheckBuilder { + return &ThirdPartyIdentityCheckBuilder{} +} + +// Build creates a new ThirdPartyIdentityCheck +func (b *ThirdPartyIdentityCheckBuilder) Build() (*ThirdPartyIdentityCheck, error) { + tpiCheck := ThirdPartyIdentityCheck{ + check: b.checkBuilder.build(), + } + + return &tpiCheck, nil +} + +func (b *ThirdPartyIdentityCheckBuilder) WithBreakdown(breakdown *report.Breakdown) *ThirdPartyIdentityCheckBuilder { + b.checkBuilder.withBreakdown(breakdown) + return b +} + +func (b *ThirdPartyIdentityCheckBuilder) WithRecommendation(recommendation *report.Recommendation) *ThirdPartyIdentityCheckBuilder { + b.checkBuilder.withRecommendation(recommendation) + return b +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/third_party_identity_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/third_party_identity_check_test.go new file mode 100644 index 0000000..97eadc1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/third_party_identity_check_test.go @@ -0,0 +1,45 @@ +package check + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" +) + +func ExampleThirdPartyIdentityCheckBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + check, err := NewThirdPartyIdentityCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/zoom_liveness_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/zoom_liveness_check.go new file mode 100644 index 0000000..0e25107 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/zoom_liveness_check.go @@ -0,0 +1,37 @@ +package check + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" +) + +// ZoomLivenessCheckBuilder builds a "ZOOM" LivenessCheck +type ZoomLivenessCheckBuilder struct { + livenessCheckBuilder +} + +// NewZoomLivenessCheckBuilder creates a new ZoomLivenessCheckBuilder +func NewZoomLivenessCheckBuilder() *ZoomLivenessCheckBuilder { + return &ZoomLivenessCheckBuilder{} +} + +// WithRecommendation sets the recommendation on the check +func (b *ZoomLivenessCheckBuilder) WithRecommendation(recommendation *report.Recommendation) *ZoomLivenessCheckBuilder { + b.livenessCheckBuilder.withRecommendation(recommendation) + return b +} + +// WithBreakdown adds a breakdown item to the check +func (b *ZoomLivenessCheckBuilder) WithBreakdown(breakdown *report.Breakdown) *ZoomLivenessCheckBuilder { + b.livenessCheckBuilder.withBreakdown(breakdown) + return b +} + +// Build creates a new LivenessCheck +func (b *ZoomLivenessCheckBuilder) Build() (*LivenessCheck, error) { + livenessCheck := b.livenessCheckBuilder. + withLivenessType(constants.Zoom). + build() + + return livenessCheck, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/zoom_liveness_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/zoom_liveness_check_test.go new file mode 100644 index 0000000..31da3b2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check/zoom_liveness_check_test.go @@ -0,0 +1,45 @@ +package check + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" +) + +func ExampleZoomLivenessCheckBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + check, err := NewZoomLivenessCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}},"liveness_type":"ZOOM"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check_reports.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check_reports.go new file mode 100644 index 0000000..6446dda --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check_reports.go @@ -0,0 +1,105 @@ +package request + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check" +) + +// CheckReports represents check reports +type CheckReports struct { + DocumentAuthenticityChecks []*check.DocumentAuthenticityCheck `json:"ID_DOCUMENT_AUTHENTICITY"` + DocumentTextDataChecks []*check.DocumentTextDataCheck `json:"ID_DOCUMENT_TEXT_DATA_CHECK"` + DocumentFaceMatchChecks []*check.DocumentFaceMatchCheck `json:"ID_DOCUMENT_FACE_MATCH"` + LivenessChecks []*check.LivenessCheck `json:"LIVENESS"` + IDDocumentComparisonChecks []*check.IDDocumentComparisonCheck `json:"ID_DOCUMENT_COMPARISON"` + SupplementaryDocumentTextDataChecks []*check.SupplementaryDocumentTextDataCheck `json:"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK"` + AsyncReportDelay uint32 `json:"async_report_delay,omitempty"` + ThirdPartyIdentityCheck *check.ThirdPartyIdentityCheck `json:"THIRD_PARTY_IDENTITY,omitempty"` +} + +// CheckReportsBuilder builds CheckReports +type CheckReportsBuilder struct { + documentAuthenticityChecks []*check.DocumentAuthenticityCheck + documentTextDataChecks []*check.DocumentTextDataCheck + documentFaceMatchChecks []*check.DocumentFaceMatchCheck + livenessChecks []*check.LivenessCheck + idDocumentComparisonChecks []*check.IDDocumentComparisonCheck + supplementaryDocumentTextDataChecks []*check.SupplementaryDocumentTextDataCheck + thirdPartyIdentityCheck *check.ThirdPartyIdentityCheck + asyncReportDelay uint32 +} + +// NewCheckReportsBuilder creates a new CheckReportsBuilder +func NewCheckReportsBuilder() *CheckReportsBuilder { + return &CheckReportsBuilder{ + documentAuthenticityChecks: []*check.DocumentAuthenticityCheck{}, + documentTextDataChecks: []*check.DocumentTextDataCheck{}, + documentFaceMatchChecks: []*check.DocumentFaceMatchCheck{}, + livenessChecks: []*check.LivenessCheck{}, + idDocumentComparisonChecks: []*check.IDDocumentComparisonCheck{}, + supplementaryDocumentTextDataChecks: []*check.SupplementaryDocumentTextDataCheck{}, + } +} + +// WithDocumentAuthenticityCheck adds a document authenticity check +func (b *CheckReportsBuilder) WithDocumentAuthenticityCheck(documentAuthenticityCheck *check.DocumentAuthenticityCheck) *CheckReportsBuilder { + b.documentAuthenticityChecks = append(b.documentAuthenticityChecks, documentAuthenticityCheck) + return b +} + +// WithDocumentTextDataCheck adds a document text data check +func (b *CheckReportsBuilder) WithDocumentTextDataCheck(documentTextDataCheck *check.DocumentTextDataCheck) *CheckReportsBuilder { + b.documentTextDataChecks = append(b.documentTextDataChecks, documentTextDataCheck) + return b +} + +// WithDocumentTextDataCheck adds a supplementary document text data check +func (b *CheckReportsBuilder) WithSupplementaryDocumentTextDataCheck(supplementaryDocumentTextDataCheck *check.SupplementaryDocumentTextDataCheck) *CheckReportsBuilder { + b.supplementaryDocumentTextDataChecks = append( + b.supplementaryDocumentTextDataChecks, + supplementaryDocumentTextDataCheck, + ) + return b +} + +// WithDocumentFaceMatchCheck adds a document face match check +func (b *CheckReportsBuilder) WithDocumentFaceMatchCheck(documentFaceMatchCheck *check.DocumentFaceMatchCheck) *CheckReportsBuilder { + b.documentFaceMatchChecks = append(b.documentFaceMatchChecks, documentFaceMatchCheck) + return b +} + +// WithLivenessCheck adds a liveness check +func (b *CheckReportsBuilder) WithLivenessCheck(livenessCheck *check.LivenessCheck) *CheckReportsBuilder { + b.livenessChecks = append(b.livenessChecks, livenessCheck) + return b +} + +// WithIDDocumentComparisonCheck adds an identity document comparison check +func (b *CheckReportsBuilder) WithIDDocumentComparisonCheck(idDocumentComparisonCheck *check.IDDocumentComparisonCheck) *CheckReportsBuilder { + b.idDocumentComparisonChecks = append(b.idDocumentComparisonChecks, idDocumentComparisonCheck) + return b +} + +// WithAsyncReportDelay sets the async report delay +func (b *CheckReportsBuilder) WithAsyncReportDelay(asyncReportDelay uint32) *CheckReportsBuilder { + b.asyncReportDelay = asyncReportDelay + return b +} + +func (b *CheckReportsBuilder) WithThirdPartyIdentityCheck(thirdPartyIdentityCheck *check.ThirdPartyIdentityCheck) *CheckReportsBuilder { + b.thirdPartyIdentityCheck = thirdPartyIdentityCheck + return b +} + +// Build creates CheckReports +func (b *CheckReportsBuilder) Build() (CheckReports, error) { + return CheckReports{ + DocumentAuthenticityChecks: b.documentAuthenticityChecks, + DocumentTextDataChecks: b.documentTextDataChecks, + DocumentFaceMatchChecks: b.documentFaceMatchChecks, + LivenessChecks: b.livenessChecks, + IDDocumentComparisonChecks: b.idDocumentComparisonChecks, + SupplementaryDocumentTextDataChecks: b.supplementaryDocumentTextDataChecks, + AsyncReportDelay: b.asyncReportDelay, + ThirdPartyIdentityCheck: b.thirdPartyIdentityCheck, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check_reports_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check_reports_test.go new file mode 100644 index 0000000..9f0b576 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/check_reports_test.go @@ -0,0 +1,130 @@ +package request + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/check/report" + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +func ExampleCheckReportsBuilder() { + breakdown, err := report.NewBreakdownBuilder(). + WithResult("some_result"). + WithSubCheck("some_check"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := report.NewRecommendationBuilder(). + WithValue("some_value"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + authenticityCheck, err := check.NewDocumentAuthenticityCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + faceMatchCheck, err := check.NewDocumentFaceMatchCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + textDataCheck, err := check.NewDocumentTextDataCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + supplementaryDocumentTextDataCheck, err := check.NewSupplementaryDocumentTextDataCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + thirdPartyCheck, err := check.NewThirdPartyIdentityCheckBuilder(). + WithBreakdown(breakdown). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + zoomLivenessCheck, err := check.NewZoomLivenessCheckBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + documentFiler, err := filter.NewDocumentFilterBuilder().Build() + idDocumentComparisonCheck, err := check.NewIDDocumentComparisonCheckBuilder(). + WithSecondaryDocumentFilter(documentFiler). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + checkReports, err := NewCheckReportsBuilder(). + WithDocumentAuthenticityCheck(authenticityCheck). + WithDocumentFaceMatchCheck(faceMatchCheck). + WithDocumentTextDataCheck(textDataCheck). + WithLivenessCheck(zoomLivenessCheck). + WithIDDocumentComparisonCheck(idDocumentComparisonCheck). + WithSupplementaryDocumentTextDataCheck(supplementaryDocumentTextDataCheck). + WithThirdPartyIdentityCheck(thirdPartyCheck). + WithAsyncReportDelay(10). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(checkReports) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"ID_DOCUMENT_AUTHENTICITY":[{"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}}}],"ID_DOCUMENT_TEXT_DATA_CHECK":[{"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}}}],"ID_DOCUMENT_FACE_MATCH":[{"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}}}],"LIVENESS":[{"result":{"report":{}},"liveness_type":"ZOOM"}],"ID_DOCUMENT_COMPARISON":[{"result":{"report":{}},"secondary_document_filter":{"document_types":[],"country_codes":[]}}],"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK":[{"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}}}],"async_report_delay":10,"THIRD_PARTY_IDENTITY":{"result":{"report":{"recommendation":{"value":"some_value"},"breakdown":[{"sub_check":"some_check","result":"some_result","details":[]}]}}}} +} + +func ExampleCheckReportsBuilder_minimal() { + checkReports, err := NewCheckReportsBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(checkReports) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"ID_DOCUMENT_AUTHENTICITY":[],"ID_DOCUMENT_TEXT_DATA_CHECK":[],"ID_DOCUMENT_FACE_MATCH":[],"LIVENESS":[],"ID_DOCUMENT_COMPARISON":[],"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK":[]} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/filter/document_filter.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/filter/document_filter.go new file mode 100644 index 0000000..e7fe86e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/filter/document_filter.go @@ -0,0 +1,53 @@ +package filter + +// DocumentFilter represents a document filter for checks and tasks +type DocumentFilter struct { + DocumentTypes []string `json:"document_types"` + CountryCodes []string `json:"country_codes"` +} + +// DocumentFilterBuilder builds a DocumentFilter +type DocumentFilterBuilder struct { + documentTypes []string + countryCodes []string +} + +// NewDocumentFilterBuilder creates a new DocumentFilterBuilder +func NewDocumentFilterBuilder() *DocumentFilterBuilder { + return &DocumentFilterBuilder{ + documentTypes: []string{}, + countryCodes: []string{}, + } +} + +// WithCountryCode adds a country code to the filter +func (b *DocumentFilterBuilder) WithCountryCode(countryCode string) *DocumentFilterBuilder { + b.countryCodes = append(b.countryCodes, countryCode) + return b +} + +// WithCountryCodes sets the country codes of the filter +func (b *DocumentFilterBuilder) WithCountryCodes(countryCodes []string) *DocumentFilterBuilder { + b.countryCodes = countryCodes + return b +} + +// WithDocumentType adds a document type to the filter +func (b *DocumentFilterBuilder) WithDocumentType(documentType string) *DocumentFilterBuilder { + b.documentTypes = append(b.documentTypes, documentType) + return b +} + +// WithDocumentTypes sets the document types of the filter +func (b *DocumentFilterBuilder) WithDocumentTypes(documentTypes []string) *DocumentFilterBuilder { + b.documentTypes = documentTypes + return b +} + +// Build creates a new DocumentFilter +func (b *DocumentFilterBuilder) Build() (*DocumentFilter, error) { + return &DocumentFilter{ + DocumentTypes: b.documentTypes, + CountryCodes: b.countryCodes, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/filter/document_filter_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/filter/document_filter_test.go new file mode 100644 index 0000000..be80e85 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/filter/document_filter_test.go @@ -0,0 +1,92 @@ +package filter + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func TestDocumentFilterBuilder_WithCountryCode(t *testing.T) { + filter, err := NewDocumentFilterBuilder(). + WithDocumentType("some_type"). + Build() + + assert.NilError(t, err) + assert.Equal(t, filter.DocumentTypes[0], "some_type") +} + +func TestDocumentFilterBuilder_WithDocumentType(t *testing.T) { + filter, err := NewDocumentFilterBuilder(). + WithCountryCode("some_country"). + Build() + + assert.NilError(t, err) + assert.Equal(t, filter.CountryCodes[0], "some_country") +} + +func ExampleDocumentFilterBuilder() { + filter, err := NewDocumentFilterBuilder(). + WithCountryCode("some_country"). + WithDocumentType("some_type"). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(filter) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_types":["some_type"],"country_codes":["some_country"]} +} + +func ExampleDocumentFilterBuilder_multipleCountriesAndDocumentTypes() { + filter, err := NewDocumentFilterBuilder(). + WithCountryCode("some_country"). + WithCountryCode("some_other_country"). + WithDocumentType("some_type"). + WithDocumentType("some_other_type"). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(filter) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_types":["some_type","some_other_type"],"country_codes":["some_country","some_other_country"]} +} + +func ExampleDocumentFilterBuilder_countriesAndDocumentTypes() { + filter, err := NewDocumentFilterBuilder(). + WithCountryCodes([]string{"some_country", "some_other_country"}). + WithDocumentTypes([]string{"some_type", "some_other_type"}). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(filter) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_types":["some_type","some_other_type"],"country_codes":["some_country","some_other_country"]} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/response_config.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/response_config.go new file mode 100644 index 0000000..a8cd28b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/response_config.go @@ -0,0 +1,48 @@ +package request + +import ( + "errors" +) + +// ResponseConfig represents the response config +type ResponseConfig struct { + TaskResults *TaskResults `json:"task_results,omitempty"` + CheckReports *CheckReports `json:"check_reports"` +} + +// ResponseConfigBuilder builds ResponseConfig +type ResponseConfigBuilder struct { + taskResults *TaskResults + checkReports *CheckReports +} + +// NewResponseConfigBuilder creates a new ResponseConfigBuilder +func NewResponseConfigBuilder() *ResponseConfigBuilder { + return &ResponseConfigBuilder{} +} + +// WithTaskResults adds task results to the response configuration +func (b *ResponseConfigBuilder) WithTaskResults(taskResults TaskResults) *ResponseConfigBuilder { + b.taskResults = &taskResults + return b +} + +// WithCheckReports adds check reports to the response configuration +func (b *ResponseConfigBuilder) WithCheckReports(checkReports CheckReports) *ResponseConfigBuilder { + b.checkReports = &checkReports + return b +} + +// Build creates ResponseConfig +func (b *ResponseConfigBuilder) Build() (*ResponseConfig, error) { + responseConfig := &ResponseConfig{ + CheckReports: b.checkReports, + TaskResults: b.taskResults, + } + + if responseConfig.CheckReports == nil { + return nil, errors.New("Check Reports must be provided") + } + + return responseConfig, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/response_config_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/response_config_test.go new file mode 100644 index 0000000..0c1b735 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/response_config_test.go @@ -0,0 +1,72 @@ +package request + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func TestResponseConfigBuilder_Build_ShouldRequireCheckReports(t *testing.T) { + _, err := NewResponseConfigBuilder().Build() + + assert.Error(t, err, "Check Reports must be provided") +} + +func ExampleResponseConfigBuilder() { + taskResults, err := NewTaskResultsBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + checkReports, err := NewCheckReportsBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + responseConfig, err := NewResponseConfigBuilder(). + WithTaskResults(taskResults). + WithCheckReports(checkReports). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(responseConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"task_results":{"ID_DOCUMENT_TEXT_DATA_EXTRACTION":[],"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION":[]},"check_reports":{"ID_DOCUMENT_AUTHENTICITY":[],"ID_DOCUMENT_TEXT_DATA_CHECK":[],"ID_DOCUMENT_FACE_MATCH":[],"LIVENESS":[],"ID_DOCUMENT_COMPARISON":[],"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK":[]}} +} + +func ExampleResponseConfigBuilder_minimal() { + checkReports, err := NewCheckReportsBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + responseConfig, err := NewResponseConfigBuilder(). + WithCheckReports(checkReports). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(responseConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"check_reports":{"ID_DOCUMENT_AUTHENTICITY":[],"ID_DOCUMENT_TEXT_DATA_CHECK":[],"ID_DOCUMENT_FACE_MATCH":[],"LIVENESS":[],"ID_DOCUMENT_COMPARISON":[],"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK":[]}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_task.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_task.go new file mode 100644 index 0000000..6c4e5b6 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_task.go @@ -0,0 +1,23 @@ +package task + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +type documentTask struct { + DocumentFilter *filter.DocumentFilter `json:"document_filter,omitempty"` +} + +type documentTaskBuilder struct { + documentFilter *filter.DocumentFilter +} + +func (b *documentTaskBuilder) withDocumentFilter(filter *filter.DocumentFilter) { + b.documentFilter = filter +} + +func (b *documentTaskBuilder) build() *documentTask { + return &documentTask{ + DocumentFilter: b.documentFilter, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_text_data_extraction_task.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_text_data_extraction_task.go new file mode 100644 index 0000000..a3dd317 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_text_data_extraction_task.go @@ -0,0 +1,94 @@ +package task + +import ( + "encoding/base64" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +// DocumentTextDataExtractionTask represents a document text data extraction task +type DocumentTextDataExtractionTask struct { + *documentTask + Result documentTextDataExtractionTaskResult `json:"result"` +} + +// DocumentTextDataExtractionTaskBuilder builds a DocumentTextDataExtractionTask +type DocumentTextDataExtractionTaskBuilder struct { + documentTaskBuilder + documentFields map[string]interface{} + documentIDPhoto *documentIDPhoto + detectedCountry string + recommendation *TextDataExtractionRecommendation +} + +type documentTextDataExtractionTaskResult struct { + DocumentFields map[string]interface{} `json:"document_fields,omitempty"` + DocumentIDPhoto *documentIDPhoto `json:"document_id_photo,omitempty"` + DetectedCountry string `json:"detected_country,omitempty"` + Recommendation *TextDataExtractionRecommendation `json:"recommendation,omitempty"` +} + +type documentIDPhoto struct { + ContentType string `json:"content_type"` + Data string `json:"data"` +} + +// NewDocumentTextDataExtractionTaskBuilder creates a new DocumentTextDataExtractionTaskBuilder +func NewDocumentTextDataExtractionTaskBuilder() *DocumentTextDataExtractionTaskBuilder { + return &DocumentTextDataExtractionTaskBuilder{} +} + +// WithDocumentFilter adds a document filter to the task +func (b *DocumentTextDataExtractionTaskBuilder) WithDocumentFilter(filter *filter.DocumentFilter) *DocumentTextDataExtractionTaskBuilder { + b.documentTaskBuilder.withDocumentFilter(filter) + return b +} + +// WithDocumentField adds a document field to the task +func (b *DocumentTextDataExtractionTaskBuilder) WithDocumentField(key string, value interface{}) *DocumentTextDataExtractionTaskBuilder { + if b.documentFields == nil { + b.documentFields = make(map[string]interface{}) + } + b.documentFields[key] = value + return b +} + +// WithDocumentFields sets document fields +func (b *DocumentTextDataExtractionTaskBuilder) WithDocumentFields(documentFields map[string]interface{}) *DocumentTextDataExtractionTaskBuilder { + b.documentFields = documentFields + return b +} + +// WithDocumentIDPhoto sets the document ID photo +func (b *DocumentTextDataExtractionTaskBuilder) WithDocumentIDPhoto(contentType string, data []byte) *DocumentTextDataExtractionTaskBuilder { + b.documentIDPhoto = &documentIDPhoto{ + ContentType: contentType, + Data: base64.StdEncoding.EncodeToString(data), + } + return b +} + +// WithDetectedCountry sets the detected country +func (b *DocumentTextDataExtractionTaskBuilder) WithDetectedCountry(detectedCountry string) *DocumentTextDataExtractionTaskBuilder { + b.detectedCountry = detectedCountry + return b +} + +// WithRecommendation sets the recommendation +func (b *DocumentTextDataExtractionTaskBuilder) WithRecommendation(recommendation *TextDataExtractionRecommendation) *DocumentTextDataExtractionTaskBuilder { + b.recommendation = recommendation + return b +} + +// Build creates a new DocumentTextDataExtractionTask +func (b *DocumentTextDataExtractionTaskBuilder) Build() (*DocumentTextDataExtractionTask, error) { + return &DocumentTextDataExtractionTask{ + documentTask: b.documentTaskBuilder.build(), + Result: documentTextDataExtractionTaskResult{ + DocumentFields: b.documentFields, + DocumentIDPhoto: b.documentIDPhoto, + DetectedCountry: b.detectedCountry, + Recommendation: b.recommendation, + }, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_text_data_extraction_task_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_text_data_extraction_task_test.go new file mode 100644 index 0000000..afc03bc --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/document_text_data_extraction_task_test.go @@ -0,0 +1,150 @@ +package task + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +func ExampleDocumentTextDataExtractionTaskBuilder() { + docFilter, err := filter.NewDocumentFilterBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + task, err := NewDocumentTextDataExtractionTaskBuilder(). + WithDocumentFilter(docFilter). + WithDocumentField("some-key", "some-value"). + WithDocumentField("some-other-key", map[string]string{ + "some-nested-key": "some-nested-value", + }). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_filter":{"document_types":[],"country_codes":[]},"result":{"document_fields":{"some-key":"some-value","some-other-key":{"some-nested-key":"some-nested-value"}}}} +} + +func ExampleDocumentTextDataExtractionTaskBuilder_WithDocumentFields() { + docFilter, err := filter.NewDocumentFilterBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + task, err := NewDocumentTextDataExtractionTaskBuilder(). + WithDocumentFilter(docFilter). + WithDocumentFields(map[string]interface{}{ + "some-key": "some-value", + "some-other-key": map[string]string{ + "some-nested-key": "some-nested-value", + }, + }). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_filter":{"document_types":[],"country_codes":[]},"result":{"document_fields":{"some-key":"some-value","some-other-key":{"some-nested-key":"some-nested-value"}}}} +} + +func ExampleDocumentTextDataExtractionTaskBuilder_WithDocumentIDPhoto() { + task, err := NewDocumentTextDataExtractionTaskBuilder(). + WithDocumentIDPhoto("some-content-type", []byte{0xDE, 0xAD, 0xBE, 0xEF}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"document_id_photo":{"content_type":"some-content-type","data":"3q2+7w=="}}} +} + +func ExampleDocumentTextDataExtractionTaskBuilder_WithDetectedCountry() { + task, err := NewDocumentTextDataExtractionTaskBuilder(). + WithDetectedCountry("some-country"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"detected_country":"some-country"}} +} + +func ExampleDocumentTextDataExtractionTaskBuilder_WithRecommendation() { + recommendation, err := NewTextDataExtractionRecommendationBuilder(). + ForProgress(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + task, err := NewDocumentTextDataExtractionTaskBuilder(). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"recommendation":{"value":"PROGRESS"}}} +} + +func ExampleDocumentTextDataExtractionTaskBuilder_minimal() { + task, err := NewDocumentTextDataExtractionTaskBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/supplementary_document_text_data_extraction_task.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/supplementary_document_text_data_extraction_task.go new file mode 100644 index 0000000..38d4e63 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/supplementary_document_text_data_extraction_task.go @@ -0,0 +1,75 @@ +package task + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +// SupplementaryDocumentTextDataExtractionTask represents a document text data extraction task +type SupplementaryDocumentTextDataExtractionTask struct { + *documentTask + Result supplementaryDocumentTextDataExtractionTaskResult `json:"result"` +} + +// SupplementaryDocumentTextDataExtractionTaskBuilder builds a SupplementaryDocumentTextDataExtractionTask +type SupplementaryDocumentTextDataExtractionTaskBuilder struct { + documentTaskBuilder + documentFields map[string]interface{} + detectedCountry string + recommendation *TextDataExtractionRecommendation +} + +type supplementaryDocumentTextDataExtractionTaskResult struct { + DocumentFields map[string]interface{} `json:"document_fields,omitempty"` + DetectedCountry string `json:"detected_country,omitempty"` + Recommendation *TextDataExtractionRecommendation `json:"recommendation,omitempty"` +} + +// NewSupplementaryDocumentTextDataExtractionTaskBuilder creates a new SupplementaryDocumentTextDataExtractionTaskBuilder +func NewSupplementaryDocumentTextDataExtractionTaskBuilder() *SupplementaryDocumentTextDataExtractionTaskBuilder { + return &SupplementaryDocumentTextDataExtractionTaskBuilder{} +} + +// WithDocumentFilter adds a document filter to the task +func (b *SupplementaryDocumentTextDataExtractionTaskBuilder) WithDocumentFilter(filter *filter.DocumentFilter) *SupplementaryDocumentTextDataExtractionTaskBuilder { + b.documentTaskBuilder.withDocumentFilter(filter) + return b +} + +// WithDocumentField adds a document field to the task +func (b *SupplementaryDocumentTextDataExtractionTaskBuilder) WithDocumentField(key string, value interface{}) *SupplementaryDocumentTextDataExtractionTaskBuilder { + if b.documentFields == nil { + b.documentFields = make(map[string]interface{}) + } + b.documentFields[key] = value + return b +} + +// WithDocumentFields sets document fields +func (b *SupplementaryDocumentTextDataExtractionTaskBuilder) WithDocumentFields(documentFields map[string]interface{}) *SupplementaryDocumentTextDataExtractionTaskBuilder { + b.documentFields = documentFields + return b +} + +// WithDetectedCountry sets the detected country +func (b *SupplementaryDocumentTextDataExtractionTaskBuilder) WithDetectedCountry(detectedCountry string) *SupplementaryDocumentTextDataExtractionTaskBuilder { + b.detectedCountry = detectedCountry + return b +} + +// WithRecommendation sets the recommendation +func (b *SupplementaryDocumentTextDataExtractionTaskBuilder) WithRecommendation(recommendation *TextDataExtractionRecommendation) *SupplementaryDocumentTextDataExtractionTaskBuilder { + b.recommendation = recommendation + return b +} + +// Build creates a new SupplementaryDocumentTextDataExtractionTask +func (b *SupplementaryDocumentTextDataExtractionTaskBuilder) Build() (*SupplementaryDocumentTextDataExtractionTask, error) { + return &SupplementaryDocumentTextDataExtractionTask{ + documentTask: b.documentTaskBuilder.build(), + Result: supplementaryDocumentTextDataExtractionTaskResult{ + DocumentFields: b.documentFields, + DetectedCountry: b.detectedCountry, + Recommendation: b.recommendation, + }, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/supplementary_document_text_data_extraction_task_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/supplementary_document_text_data_extraction_task_test.go new file mode 100644 index 0000000..31c73b0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/supplementary_document_text_data_extraction_task_test.go @@ -0,0 +1,131 @@ +package task + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/filter" +) + +func ExampleSupplementaryDocumentTextDataExtractionTaskBuilder() { + docFilter, err := filter.NewDocumentFilterBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + task, err := NewSupplementaryDocumentTextDataExtractionTaskBuilder(). + WithDocumentFilter(docFilter). + WithDocumentField("some-key", "some-value"). + WithDocumentField("some-other-key", map[string]string{ + "some-nested-key": "some-nested-value", + }). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_filter":{"document_types":[],"country_codes":[]},"result":{"document_fields":{"some-key":"some-value","some-other-key":{"some-nested-key":"some-nested-value"}}}} +} + +func ExampleSupplementaryDocumentTextDataExtractionTaskBuilder_WithDocumentFields() { + docFilter, err := filter.NewDocumentFilterBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + task, err := NewSupplementaryDocumentTextDataExtractionTaskBuilder(). + WithDocumentFilter(docFilter). + WithDocumentFields(map[string]interface{}{ + "some-key": "some-value", + "some-other-key": map[string]string{ + "some-nested-key": "some-nested-value", + }, + }). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_filter":{"document_types":[],"country_codes":[]},"result":{"document_fields":{"some-key":"some-value","some-other-key":{"some-nested-key":"some-nested-value"}}}} +} + +func ExampleSupplementaryDocumentTextDataExtractionTaskBuilder_WithDetectedCountry() { + task, err := NewSupplementaryDocumentTextDataExtractionTaskBuilder(). + WithDetectedCountry("some-country"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"detected_country":"some-country"}} +} + +func ExampleSupplementaryDocumentTextDataExtractionTaskBuilder_WithRecommendation() { + recommendation, err := NewTextDataExtractionRecommendationBuilder(). + ForProgress(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + task, err := NewSupplementaryDocumentTextDataExtractionTaskBuilder(). + WithRecommendation(recommendation). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{"recommendation":{"value":"PROGRESS"}}} +} + +func ExampleSupplementaryDocumentTextDataExtractionTaskBuilder_minimal() { + task, err := NewSupplementaryDocumentTextDataExtractionTaskBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"result":{}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_reason.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_reason.go new file mode 100644 index 0000000..9b6b180 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_reason.go @@ -0,0 +1,49 @@ +package task + +const ( + valueQuality string = "QUALITY" + valueUserError string = "USER_ERROR" +) + +// TextDataExtractionReason represents a text data extraction reason +type TextDataExtractionReason struct { + Value string `json:"value"` + Detail string `json:"detail,omitempty"` +} + +// TextDataExtractionReasonBuilder builds a TextDataExtractionReason +type TextDataExtractionReasonBuilder struct { + value string + detail string +} + +// NewTextDataExtractionReasonBuilder creates a new TextDataExtractionReasonBuilder +func NewTextDataExtractionReasonBuilder() *TextDataExtractionReasonBuilder { + return &TextDataExtractionReasonBuilder{} +} + +// ForQuality sets the reason to quality +func (b *TextDataExtractionReasonBuilder) ForQuality() *TextDataExtractionReasonBuilder { + b.value = valueQuality + return b +} + +// ForUserError sets the reason to user error +func (b *TextDataExtractionReasonBuilder) ForUserError() *TextDataExtractionReasonBuilder { + b.value = valueUserError + return b +} + +// WithDetail sets the reason detail +func (b *TextDataExtractionReasonBuilder) WithDetail(detail string) *TextDataExtractionReasonBuilder { + b.detail = detail + return b +} + +// Build creates a new TextDataExtractionReason +func (b *TextDataExtractionReasonBuilder) Build() (*TextDataExtractionReason, error) { + return &TextDataExtractionReason{ + Detail: b.detail, + Value: b.value, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_reason_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_reason_test.go new file mode 100644 index 0000000..ac64ed4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_reason_test.go @@ -0,0 +1,63 @@ +package task + +import ( + "encoding/json" + "fmt" +) + +func ExampleTextDataExtractionReasonBuilder() { + reason, err := NewTextDataExtractionReasonBuilder(). + ForQuality(). + WithDetail("some-detail"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(reason) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"QUALITY","detail":"some-detail"} +} + +func ExampleTextDataExtractionReasonBuilder_ForQuality() { + reason, err := NewTextDataExtractionReasonBuilder(). + ForQuality(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(reason) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"QUALITY"} +} +func ExampleTextDataExtractionReasonBuilder_ForUserError() { + reason, err := NewTextDataExtractionReasonBuilder(). + ForUserError(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(reason) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"USER_ERROR"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_recommendation.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_recommendation.go new file mode 100644 index 0000000..4daf35c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_recommendation.go @@ -0,0 +1,56 @@ +package task + +const ( + valueProgress string = "PROGRESS" + valueMustTryAgain string = "MUST_TRY_AGAIN" + valueShouldTryAgain string = "SHOULD_TRY_AGAIN" +) + +// TextDataExtractionRecommendation represents a text data extraction reason +type TextDataExtractionRecommendation struct { + Value string `json:"value"` + Reason *TextDataExtractionReason `json:"reason,omitempty"` +} + +// TextDataExtractionRecommendationBuilder builds a TextDataExtractionRecommendation +type TextDataExtractionRecommendationBuilder struct { + value string + reason *TextDataExtractionReason +} + +// NewTextDataExtractionRecommendationBuilder creates a new TextDataExtractionRecommendationBuilder +func NewTextDataExtractionRecommendationBuilder() *TextDataExtractionRecommendationBuilder { + return &TextDataExtractionRecommendationBuilder{} +} + +// ForProgress sets the recommendation value to progress +func (b *TextDataExtractionRecommendationBuilder) ForProgress() *TextDataExtractionRecommendationBuilder { + b.value = valueProgress + return b +} + +// ForMustTryAgain sets the recommendation value to must try again +func (b *TextDataExtractionRecommendationBuilder) ForMustTryAgain() *TextDataExtractionRecommendationBuilder { + b.value = valueMustTryAgain + return b +} + +// ForShouldTryAgain sets the recommendation value to should try again +func (b *TextDataExtractionRecommendationBuilder) ForShouldTryAgain() *TextDataExtractionRecommendationBuilder { + b.value = valueShouldTryAgain + return b +} + +// WithReason sets the recommendation reason +func (b *TextDataExtractionRecommendationBuilder) WithReason(reason *TextDataExtractionReason) *TextDataExtractionRecommendationBuilder { + b.reason = reason + return b +} + +// Build creates a new TextDataExtractionRecommendation +func (b *TextDataExtractionRecommendationBuilder) Build() (*TextDataExtractionRecommendation, error) { + return &TextDataExtractionRecommendation{ + Value: b.value, + Reason: b.reason, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_recommendation_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_recommendation_test.go new file mode 100644 index 0000000..5c4dcb1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task/text_data_extraction_recommendation_test.go @@ -0,0 +1,91 @@ +package task + +import ( + "encoding/json" + "fmt" +) + +func ExampleTextDataExtractionRecommendationBuilder() { + reason, err := NewTextDataExtractionReasonBuilder(). + ForQuality(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + recommendation, err := NewTextDataExtractionRecommendationBuilder(). + ForShouldTryAgain(). + WithReason(reason). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(recommendation) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"SHOULD_TRY_AGAIN","reason":{"value":"QUALITY"}} +} + +func ExampleTextDataExtractionRecommendationBuilder_ForProgress() { + recommendation, err := NewTextDataExtractionRecommendationBuilder(). + ForProgress(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(recommendation) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"PROGRESS"} +} + +func ExampleTextDataExtractionRecommendationBuilder_ForShouldTryAgain() { + recommendation, err := NewTextDataExtractionRecommendationBuilder(). + ForShouldTryAgain(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(recommendation) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"SHOULD_TRY_AGAIN"} +} + +func ExampleTextDataExtractionRecommendationBuilder_ForMustTryAgain() { + recommendation, err := NewTextDataExtractionRecommendationBuilder(). + ForMustTryAgain(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(recommendation) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"value":"MUST_TRY_AGAIN"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task_results.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task_results.go new file mode 100644 index 0000000..43dc4ca --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task_results.go @@ -0,0 +1,45 @@ +package request + +import ( + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/task" +) + +// TaskResults represents task results +type TaskResults struct { + DocumentTextDataExtractionTasks []*task.DocumentTextDataExtractionTask `json:"ID_DOCUMENT_TEXT_DATA_EXTRACTION"` + SupplementaryDocumentTextDataExtractionTasks []*task.SupplementaryDocumentTextDataExtractionTask `json:"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION"` +} + +// TaskResultsBuilder builds TaskResults +type TaskResultsBuilder struct { + documentTextDataExtractionTasks []*task.DocumentTextDataExtractionTask + supplementaryDocumentTextDataExtractionTasks []*task.SupplementaryDocumentTextDataExtractionTask +} + +// NewTaskResultsBuilder creates a new TaskResultsBuilder +func NewTaskResultsBuilder() *TaskResultsBuilder { + return &TaskResultsBuilder{ + documentTextDataExtractionTasks: []*task.DocumentTextDataExtractionTask{}, + supplementaryDocumentTextDataExtractionTasks: []*task.SupplementaryDocumentTextDataExtractionTask{}, + } +} + +// WithDocumentTextDataExtractionTask adds a supplementary document text data extraction task +func (b *TaskResultsBuilder) WithDocumentTextDataExtractionTask(documentTextDataExtractionTask *task.DocumentTextDataExtractionTask) *TaskResultsBuilder { + b.documentTextDataExtractionTasks = append(b.documentTextDataExtractionTasks, documentTextDataExtractionTask) + return b +} + +// WithSupplementaryDocumentTextDataExtractionTask adds a supplementary document text data extraction task +func (b *TaskResultsBuilder) WithSupplementaryDocumentTextDataExtractionTask(supplementaryTextDataExtractionTask *task.SupplementaryDocumentTextDataExtractionTask) *TaskResultsBuilder { + b.supplementaryDocumentTextDataExtractionTasks = append(b.supplementaryDocumentTextDataExtractionTasks, supplementaryTextDataExtractionTask) + return b +} + +// Build creates TaskResults +func (b *TaskResultsBuilder) Build() (TaskResults, error) { + return TaskResults{ + DocumentTextDataExtractionTasks: b.documentTextDataExtractionTasks, + SupplementaryDocumentTextDataExtractionTasks: b.supplementaryDocumentTextDataExtractionTasks, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task_results_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task_results_test.go new file mode 100644 index 0000000..c8b89b8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/sandbox/request/task_results_test.go @@ -0,0 +1,42 @@ +package request + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/sandbox/request/task" +) + +func ExampleTaskResultsBuilder() { + textDataExtractionTask, err := task.NewDocumentTextDataExtractionTaskBuilder(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + supplementaryTextDataExtractionTask, err := task.NewSupplementaryDocumentTextDataExtractionTaskBuilder(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + taskResults, err := NewTaskResultsBuilder(). + WithDocumentTextDataExtractionTask(textDataExtractionTask). + WithSupplementaryDocumentTextDataExtractionTask(supplementaryTextDataExtractionTask). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(taskResults) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"ID_DOCUMENT_TEXT_DATA_EXTRACTION":[{"result":{}}],"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION":[{"result":{}}]} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/constants.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/constants.go new file mode 100644 index 0000000..4b40973 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/constants.go @@ -0,0 +1,6 @@ +package check + +const ( + zoom = "ZOOM" + static = "STATIC" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_authenticity.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_authenticity.go new file mode 100644 index 0000000..8cd36ed --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_authenticity.go @@ -0,0 +1,75 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedDocumentAuthenticityCheck requests creation of a Document Authenticity Check +type RequestedDocumentAuthenticityCheck struct { + config RequestedDocumentAuthenticityConfig +} + +// Type is the type of the Requested Check +func (c *RequestedDocumentAuthenticityCheck) Type() string { + return constants.IDDocumentAuthenticity +} + +// Config is the configuration of the Requested Check +func (c *RequestedDocumentAuthenticityCheck) Config() RequestedCheckConfig { + return RequestedCheckConfig( + c.config, + ) +} + +// MarshalJSON returns the JSON encoding +func (c *RequestedDocumentAuthenticityCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} + +// RequestedDocumentAuthenticityConfig is the configuration applied when creating a Document Authenticity Check +type RequestedDocumentAuthenticityConfig struct { + ManualCheck string `json:"manual_check,omitempty"` +} + +// RequestedDocumentAuthenticityCheckBuilder builds a RequestedDocumentAuthenticityCheck +type RequestedDocumentAuthenticityCheckBuilder struct { + config RequestedDocumentAuthenticityConfig +} + +// NewRequestedDocumentAuthenticityCheckBuilder creates a new DocumentAuthenticityCheckBuilder +func NewRequestedDocumentAuthenticityCheckBuilder() *RequestedDocumentAuthenticityCheckBuilder { + return &RequestedDocumentAuthenticityCheckBuilder{} +} + +// WithManualCheckAlways requires that a manual follow-up check is always performed +func (b *RequestedDocumentAuthenticityCheckBuilder) WithManualCheckAlways() *RequestedDocumentAuthenticityCheckBuilder { + b.config.ManualCheck = constants.Always + return b +} + +// WithManualCheckFallback requires that a manual follow-up check is performed only on failed Checks, and those with a low level of confidence +func (b *RequestedDocumentAuthenticityCheckBuilder) WithManualCheckFallback() *RequestedDocumentAuthenticityCheckBuilder { + b.config.ManualCheck = constants.Fallback + return b +} + +// WithManualCheckNever requires that only an automated Check is performed. No manual follow-up Check will ever be initiated +func (b *RequestedDocumentAuthenticityCheckBuilder) WithManualCheckNever() *RequestedDocumentAuthenticityCheckBuilder { + b.config.ManualCheck = constants.Never + return b +} + +// Build builds the RequestedDocumentAuthenticityCheck +func (b *RequestedDocumentAuthenticityCheckBuilder) Build() (*RequestedDocumentAuthenticityCheck, error) { + return &RequestedDocumentAuthenticityCheck{ + config: b.config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_authenticity_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_authenticity_test.go new file mode 100644 index 0000000..36afa71 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_authenticity_test.go @@ -0,0 +1,102 @@ +package check + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleRequestedDocumentAuthenticityCheckBuilder() { + docAuthCheck, err := NewRequestedDocumentAuthenticityCheckBuilder().WithManualCheckFallback().Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(docAuthCheck) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ID_DOCUMENT_AUTHENTICITY","config":{"manual_check":"FALLBACK"}} +} + +func ExampleRequestedDocumentAuthenticityCheckBuilder_Build() { + docAuthCheck, err := NewRequestedDocumentAuthenticityCheckBuilder().Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(docAuthCheck) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ID_DOCUMENT_AUTHENTICITY","config":{}} +} + +func TestRequestedDocumentAuthenticityCheckBuilder_WithManualCheckAlways(t *testing.T) { + docAuthCheck, err := NewRequestedDocumentAuthenticityCheckBuilder(). + WithManualCheckAlways(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + result := docAuthCheck.Config().(RequestedDocumentAuthenticityConfig) + assert.Equal(t, "ALWAYS", result.ManualCheck) +} + +func TestRequestedDocumentAuthenticityCheckBuilder_WithManualCheckFallback(t *testing.T) { + docAuthCheck, err := NewRequestedDocumentAuthenticityCheckBuilder(). + WithManualCheckFallback(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + result := docAuthCheck.Config().(RequestedDocumentAuthenticityConfig) + assert.Equal(t, "FALLBACK", result.ManualCheck) +} + +func TestRequestedDocumentAuthenticityCheckBuilder_WithManualCheckNever(t *testing.T) { + docAuthCheck, err := NewRequestedDocumentAuthenticityCheckBuilder(). + WithManualCheckNever(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + result := docAuthCheck.Config().(RequestedDocumentAuthenticityConfig) + assert.Equal(t, "NEVER", result.ManualCheck) +} + +func TestRequestedDocumentAuthenticityCheckBuilder_UsesLastValue(t *testing.T) { + docAuthCheck, err := NewRequestedDocumentAuthenticityCheckBuilder(). + WithManualCheckFallback(). + WithManualCheckNever(). + WithManualCheckAlways(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + result := docAuthCheck.Config().(RequestedDocumentAuthenticityConfig) + assert.Equal(t, "ALWAYS", result.ManualCheck) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_comparison.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_comparison.go new file mode 100644 index 0000000..6c5b06b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_comparison.go @@ -0,0 +1,56 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedIDDocumentComparisonCheck requests creation of a Document Comparison Check +type RequestedIDDocumentComparisonCheck struct { + config RequestedIDDocumentComparisonConfig +} + +// Type is the type of the Requested Check +func (c *RequestedIDDocumentComparisonCheck) Type() string { + return constants.IDDocumentComparison +} + +// Config is the configuration of the Requested Check +func (c *RequestedIDDocumentComparisonCheck) Config() RequestedCheckConfig { + return RequestedCheckConfig( + c.config, + ) +} + +// MarshalJSON returns the JSON encoding +func (c *RequestedIDDocumentComparisonCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} + +// RequestedIDDocumentComparisonConfig is the configuration applied when creating a Document Comparison Check +type RequestedIDDocumentComparisonConfig struct { +} + +// RequestedIDDocumentComparisonCheckBuilder builds a RequestedIDDocumentComparisonCheck +type RequestedIDDocumentComparisonCheckBuilder struct { + config RequestedIDDocumentComparisonConfig +} + +// NewRequestedIDDocumentComparisonCheckBuilder creates a new DocumentComparisonCheckBuilder +func NewRequestedIDDocumentComparisonCheckBuilder() *RequestedIDDocumentComparisonCheckBuilder { + return &RequestedIDDocumentComparisonCheckBuilder{} +} + +// Build builds the RequestedIDDocumentComparisonCheck +func (b *RequestedIDDocumentComparisonCheckBuilder) Build() (*RequestedIDDocumentComparisonCheck, error) { + return &RequestedIDDocumentComparisonCheck{ + config: b.config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_comparison_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_comparison_test.go new file mode 100644 index 0000000..1fa0752 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/document_comparison_test.go @@ -0,0 +1,24 @@ +package check + +import ( + "encoding/json" + "fmt" +) + +func ExampleNewRequestedIDDocumentComparisonCheckBuilder() { + check, err := NewRequestedIDDocumentComparisonCheckBuilder().Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ID_DOCUMENT_COMPARISON","config":{}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check.go new file mode 100644 index 0000000..caf783c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check.go @@ -0,0 +1,33 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedFaceComparisonCheck represents a face comparison check request. +type RequestedFaceComparisonCheck struct { + config RequestedFaceComparisonConfig +} + +// Type returns the type of the check. +func (c *RequestedFaceComparisonCheck) Type() string { + return constants.FaceComparison +} + +// Config returns the configuration of the check. +func (c *RequestedFaceComparisonCheck) Config() RequestedCheckConfig { + return c.config +} + +// MarshalJSON encodes the struct into JSON. +func (c *RequestedFaceComparisonCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check_builder.go new file mode 100644 index 0000000..94cceb9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check_builder.go @@ -0,0 +1,39 @@ +package check + +import "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" + +// RequestedFaceComparisonCheckBuilder builds a RequestedFaceComparisonCheck. +type RequestedFaceComparisonCheckBuilder struct { + manualCheck string +} + +// NewRequestedFaceComparisonCheckBuilder creates a new builder. +func NewRequestedFaceComparisonCheckBuilder() *RequestedFaceComparisonCheckBuilder { + return &RequestedFaceComparisonCheckBuilder{} +} + +// WithManualCheckNever sets the manual check mode to NEVER. +func (b *RequestedFaceComparisonCheckBuilder) WithManualCheckNever() *RequestedFaceComparisonCheckBuilder { + b.manualCheck = constants.Never + return b +} + +func (b *RequestedFaceComparisonCheckBuilder) WithManualCheckAlways() *RequestedFaceComparisonCheckBuilder { + b.manualCheck = constants.Always + return b +} + +func (b *RequestedFaceComparisonCheckBuilder) WithManualCheckFallback() *RequestedFaceComparisonCheckBuilder { + b.manualCheck = constants.Fallback + return b +} + +// Build constructs the RequestedFaceComparisonCheck. +func (b *RequestedFaceComparisonCheckBuilder) Build() (*RequestedFaceComparisonCheck, error) { + config := RequestedFaceComparisonConfig{ + ManualCheck: b.manualCheck, + } + return &RequestedFaceComparisonCheck{ + config: config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check_test.go new file mode 100644 index 0000000..1e18707 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_check_test.go @@ -0,0 +1,72 @@ +package check + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleRequestedFaceComparisonCheckBuilder() { + check, err := NewRequestedFaceComparisonCheckBuilder(). + WithManualCheckNever(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"FACE_COMPARISON","config":{"manual_check":"NEVER"}} +} + +func TestRequestedFaceComparisonCheckBuilder_WithManualCheckAlways(t *testing.T) { + check, err := NewRequestedFaceComparisonCheckBuilder(). + WithManualCheckAlways(). + Build() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + config := check.Config().(RequestedFaceComparisonConfig) + assert.Equal(t, "ALWAYS", config.ManualCheck) +} + +func TestRequestedFaceComparisonCheckBuilder_WithManualCheckFallback(t *testing.T) { + check, err := NewRequestedFaceComparisonCheckBuilder(). + WithManualCheckFallback(). + Build() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + config := check.Config().(RequestedFaceComparisonConfig) + assert.Equal(t, "FALLBACK", config.ManualCheck) +} + +func TestRequestedFaceComparisonCheckBuilder_WithManualCheckNever(t *testing.T) { + check, err := NewRequestedFaceComparisonCheckBuilder(). + WithManualCheckNever(). + Build() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + config := check.Config().(RequestedFaceComparisonConfig) + assert.Equal(t, "NEVER", config.ManualCheck) +} + +func TestRequestedFaceComparisonCheckBuilder_DefaultManualCheckEmpty(t *testing.T) { + builder := NewRequestedFaceComparisonCheckBuilder() + + check, err := builder.Build() + assert.NilError(t, err) + assert.Equal(t, check.config.ManualCheck, "") // default is empty string +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_config.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_config.go new file mode 100644 index 0000000..9b77cca --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_comparison_config.go @@ -0,0 +1,6 @@ +package check + +// RequestedFaceComparisonConfig is the configuration for a face comparison check. +type RequestedFaceComparisonConfig struct { + ManualCheck string `json:"manual_check,omitempty"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_match.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_match.go new file mode 100644 index 0000000..8142e61 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_match.go @@ -0,0 +1,77 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedFaceMatchCheck requests creation of a FaceMatch Check +type RequestedFaceMatchCheck struct { + config RequestedFaceMatchConfig +} + +// Type is the type of the Requested Check +func (c *RequestedFaceMatchCheck) Type() string { + return constants.IDDocumentFaceMatch +} + +// Config is the configuration of the Requested Check +func (c *RequestedFaceMatchCheck) Config() RequestedCheckConfig { + return c.config +} + +// MarshalJSON returns the JSON encoding +func (c *RequestedFaceMatchCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} + +// RequestedFaceMatchConfig is the configuration applied when creating a FaceMatch Check +type RequestedFaceMatchConfig struct { + ManualCheck string `json:"manual_check,omitempty"` +} + +// NewRequestedFaceMatchCheckBuilder creates a new RequestedFaceMatchCheckBuilder +func NewRequestedFaceMatchCheckBuilder() *RequestedFaceMatchCheckBuilder { + return &RequestedFaceMatchCheckBuilder{} +} + +// RequestedFaceMatchCheckBuilder builds a RequestedFaceMatchCheck +type RequestedFaceMatchCheckBuilder struct { + manualCheck string +} + +// WithManualCheckAlways sets the value of manual check to "ALWAYS" +func (b *RequestedFaceMatchCheckBuilder) WithManualCheckAlways() *RequestedFaceMatchCheckBuilder { + b.manualCheck = constants.Always + return b +} + +// WithManualCheckFallback sets the value of manual check to "FALLBACK" +func (b *RequestedFaceMatchCheckBuilder) WithManualCheckFallback() *RequestedFaceMatchCheckBuilder { + b.manualCheck = constants.Fallback + return b +} + +// WithManualCheckNever sets the value of manual check to "NEVER" +func (b *RequestedFaceMatchCheckBuilder) WithManualCheckNever() *RequestedFaceMatchCheckBuilder { + b.manualCheck = constants.Never + return b +} + +// Build builds the RequestedFaceMatchCheck +func (b *RequestedFaceMatchCheckBuilder) Build() (*RequestedFaceMatchCheck, error) { + config := RequestedFaceMatchConfig{ + ManualCheck: b.manualCheck, + } + + return &RequestedFaceMatchCheck{ + config: config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_match_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_match_test.go new file mode 100644 index 0000000..6686d66 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/face_match_test.go @@ -0,0 +1,64 @@ +package check + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleRequestedFaceMatchCheckBuilder() { + check, err := NewRequestedFaceMatchCheckBuilder(). + WithManualCheckNever(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ID_DOCUMENT_FACE_MATCH","config":{"manual_check":"NEVER"}} +} + +func TestRequestedFaceMatchCheckBuilder_WithManualCheckAlways(t *testing.T) { + task, err := NewRequestedFaceMatchCheckBuilder(). + WithManualCheckAlways(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedFaceMatchConfig) + assert.Equal(t, "ALWAYS", config.ManualCheck) +} + +func TestRequestedFaceMatchCheckBuilder_WithManualCheckFallback(t *testing.T) { + task, err := NewRequestedFaceMatchCheckBuilder(). + WithManualCheckFallback(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedFaceMatchConfig) + assert.Equal(t, "FALLBACK", config.ManualCheck) +} + +func TestRequestedFaceMatchCheckBuilder_WithManualCheckNever(t *testing.T) { + task, err := NewRequestedFaceMatchCheckBuilder(). + WithManualCheckNever(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedFaceMatchConfig) + assert.Equal(t, "NEVER", config.ManualCheck) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/liveness.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/liveness.go new file mode 100644 index 0000000..0651946 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/liveness.go @@ -0,0 +1,98 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedLivenessCheck requests creation of a Liveness Check +type RequestedLivenessCheck struct { + config RequestedLivenessConfig +} + +// Type is the type of the Requested Check +func (c *RequestedLivenessCheck) Type() string { + return constants.Liveness +} + +// Config is the configuration of the Requested Check +func (c *RequestedLivenessCheck) Config() RequestedCheckConfig { + return RequestedCheckConfig( + c.config, + ) +} + +// MarshalJSON returns the JSON encoding +func (c *RequestedLivenessCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} + +// RequestedLivenessConfig is the configuration applied when creating a Liveness Check +type RequestedLivenessConfig struct { + MaxRetries int `json:"max_retries,omitempty"` + LivenessType string `json:"liveness_type,omitempty"` + ManualCheck string `json:"manual_check,omitempty"` +} + +// NewRequestedLivenessCheckBuilder creates a new RequestedLivenessCheckBuilder +func NewRequestedLivenessCheckBuilder() *RequestedLivenessCheckBuilder { + return &RequestedLivenessCheckBuilder{} +} + +// RequestedLivenessCheckBuilder builds a RequestedLivenessCheck +type RequestedLivenessCheckBuilder struct { + livenessType string + maxRetries int + manualCheck string +} + +// ForZoomLiveness sets the liveness type to "ZOOM" +func (b *RequestedLivenessCheckBuilder) ForZoomLiveness() *RequestedLivenessCheckBuilder { + return b.ForLivenessType(zoom) +} + +// ForStaticLiveness sets the liveness type to "STATIC" +func (b *RequestedLivenessCheckBuilder) ForStaticLiveness() *RequestedLivenessCheckBuilder { + return b.ForLivenessType(static) +} + +// ForLivenessType sets the liveness type on the builder +func (b *RequestedLivenessCheckBuilder) ForLivenessType(livenessType string) *RequestedLivenessCheckBuilder { + b.livenessType = livenessType + if livenessType == constants.Static { + b.WithManualCheckNever() + } + return b +} + +// WithMaxRetries sets the maximum number of retries allowed for liveness check on the builder +func (b *RequestedLivenessCheckBuilder) WithMaxRetries(maxRetries int) *RequestedLivenessCheckBuilder { + b.maxRetries = maxRetries + return b +} + +// WithManualCheckNever sets the value of manual check to "NEVER" +func (b *RequestedLivenessCheckBuilder) WithManualCheckNever() *RequestedLivenessCheckBuilder { + b.manualCheck = constants.Never + return b +} + +// Build builds the RequestedLivenessCheck +func (b *RequestedLivenessCheckBuilder) Build() (*RequestedLivenessCheck, error) { + config := RequestedLivenessConfig{ + MaxRetries: b.maxRetries, + LivenessType: b.livenessType, + ManualCheck: b.manualCheck, + } + + return &RequestedLivenessCheck{ + config: config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/liveness_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/liveness_test.go new file mode 100644 index 0000000..ae03035 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/liveness_test.go @@ -0,0 +1,64 @@ +package check + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleRequestedLivenessCheckBuilder() { + check, err := NewRequestedLivenessCheckBuilder(). + ForZoomLiveness(). + WithMaxRetries(9). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"LIVENESS","config":{"max_retries":9,"liveness_type":"ZOOM"}} + +} + +func TestExampleRequestedStaticLivenessCheckBuilder(t *testing.T) { + check, err := NewRequestedLivenessCheckBuilder(). + ForStaticLiveness(). + WithMaxRetries(5). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(check) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"LIVENESS","config":{"max_retries":5,"liveness_type":"STATIC"}} +} + +func TestRequestedLivenessCheckBuilder_MaxRetriesIsOmittedIfNotSet(t *testing.T) { + check, err := NewRequestedLivenessCheckBuilder(). + ForLivenessType("LIVENESS_TYPE"). + Build() + + assert.NilError(t, err) + + result, err := json.Marshal(check) + assert.NilError(t, err) + + expected := "{\"type\":\"LIVENESS\",\"config\":{\"liveness_type\":\"LIVENESS_TYPE\"}}" + assert.Equal(t, expected, string(result)) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/requested_check.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/requested_check.go new file mode 100644 index 0000000..2da6f9d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/requested_check.go @@ -0,0 +1,12 @@ +package check + +// RequestedCheck requests creation of a Check to be performed on a document +type RequestedCheck interface { + Type() string + Config() RequestedCheckConfig + MarshalJSON() ([]byte, error) +} + +// RequestedCheckConfig is the configuration applied when creating a Check +type RequestedCheckConfig interface { +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/third_party_identity.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/third_party_identity.go new file mode 100644 index 0000000..6af6d40 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/third_party_identity.go @@ -0,0 +1,55 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedThirdPartyIdentityCheck requests creation of a third party CRA check +type RequestedThirdPartyIdentityCheck struct { + config RequestedThirdPartyIdentityCheckConfig +} + +// Type is the type of the requested check +func (c *RequestedThirdPartyIdentityCheck) Type() string { + return constants.ThirdPartyIdentityCheck +} + +// Config is the configuration of the requested check +func (c *RequestedThirdPartyIdentityCheck) Config() RequestedCheckConfig { + return RequestedCheckConfig(c.config) +} + +// MarshalJSON returns the JSON encoding +func (c *RequestedThirdPartyIdentityCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} + +// RequestedThirdPartyIdentityCheckConfig is the configuration applied when creating +// a third party identity check +type RequestedThirdPartyIdentityCheckConfig struct { +} + +// RequestedThirdPartyIdentityCheckBuilder builds a RequestedThirdPartyIdentityCheck +type RequestedThirdPartyIdentityCheckBuilder struct { + config RequestedThirdPartyIdentityCheckConfig +} + +// NewRequestedThirdPartyIdentityCheckBuilder creates a new builder for RequestedThirdPartyIdentityCheck +func NewRequestedThirdPartyIdentityCheckBuilder() *RequestedThirdPartyIdentityCheckBuilder { + return &RequestedThirdPartyIdentityCheckBuilder{} +} + +// Build builds the RequestedThirdPartyIdentityCheck +func (b *RequestedThirdPartyIdentityCheckBuilder) Build() (*RequestedThirdPartyIdentityCheck, error) { + return &RequestedThirdPartyIdentityCheck{ + config: b.config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/third_party_identity_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/third_party_identity_test.go new file mode 100644 index 0000000..4a8c7fc --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/third_party_identity_test.go @@ -0,0 +1,23 @@ +package check + +import ( + "encoding/json" + "fmt" +) + +func ExampleRequestedThirdPartyIdentityCheck() { + thirdPartyCheck, err := NewRequestedThirdPartyIdentityCheckBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(thirdPartyCheck) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"THIRD_PARTY_IDENTITY","config":{}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca.go new file mode 100644 index 0000000..c2ccd69 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca.go @@ -0,0 +1,9 @@ +package check + +type RequestedWatchlistAdvancedCAConfig struct { + Type string `json:"type,omitempty"` + RemoveDeceased bool `json:"remove_deceased,omitempty"` + ShareUrl bool `json:"share_url,omitempty"` + Sources RequestedCASources `json:"sources,omitempty"` + MatchingStrategy RequestedCAMatchingStrategy `json:"matching_strategy,omitempty"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_custom.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_custom.go new file mode 100644 index 0000000..61e6aa3 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_custom.go @@ -0,0 +1,120 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +type RequestedWatchlistAdvancedCACustomAccountCheck struct { + config RequestedWatchlistAdvancedCACustomAccountConfig +} + +// Type is the type of the requested check +func (c RequestedWatchlistAdvancedCACustomAccountCheck) Type() string { + return constants.WatchlistAdvancedCA +} + +// Config is the configuration of the requested check +func (c RequestedWatchlistAdvancedCACustomAccountCheck) Config() RequestedCheckConfig { + return RequestedCheckConfig(c.config) +} + +// MarshalJSON returns the JSON encoding +func (c RequestedWatchlistAdvancedCACustomAccountCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} + +func NewRequestedWatchlistAdvancedCACheckCustomAccountBuilder() *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + return &RequestedWatchlistAdvancedCACheckCustomAccountBuilder{} +} + +type RequestedWatchlistAdvancedCACustomAccountConfig struct { + RequestedWatchlistAdvancedCAConfig + APIKey string `json:"api_key,omitempty"` + Monitoring bool `json:"monitoring,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + ClientRef string `json:"client_ref,omitempty"` +} + +type RequestedWatchlistAdvancedCACheckCustomAccountBuilder struct { + removeDeceased bool + shareURL bool + requestedCASources RequestedCASources + requestedCAMatchingStrategy RequestedCAMatchingStrategy + apiKey string + monitoring bool + tags map[string]string + clientRef string +} + +// WithAPIKey sets the API key for the Watchlist Advanced CA check (custom account). +func (b *RequestedWatchlistAdvancedCACheckCustomAccountBuilder) WithAPIKey(apiKey string) *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + b.apiKey = apiKey + return b +} + +// WithMonitoring sets whether monitoring is used for the Watchlist Advanced CA check (custom account). +func (b *RequestedWatchlistAdvancedCACheckCustomAccountBuilder) WithMonitoring(monitoring bool) *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + b.monitoring = monitoring + return b +} + +// WithTags sets tags used for custom account Watchlist Advanced CA check. +// Please note this will override any previously set tags +func (b *RequestedWatchlistAdvancedCACheckCustomAccountBuilder) WithTags(tags map[string]string) *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + b.tags = tags + return b +} + +// WithClientRef sets the client reference for the Watchlist Advanced CA check (custom account). +func (b *RequestedWatchlistAdvancedCACheckCustomAccountBuilder) WithClientRef(clientRef string) *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + b.clientRef = clientRef + return b +} + +func (b *RequestedWatchlistAdvancedCACheckCustomAccountBuilder) WithRemoveDeceased(removeDeceased bool) *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + b.removeDeceased = removeDeceased + return b +} + +func (b *RequestedWatchlistAdvancedCACheckCustomAccountBuilder) WithShareURL(shareURL bool) *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + b.shareURL = shareURL + return b +} + +func (b *RequestedWatchlistAdvancedCACheckCustomAccountBuilder) WithSources(requestedCASources RequestedCASources) *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + b.requestedCASources = requestedCASources + return b +} + +func (b *RequestedWatchlistAdvancedCACheckCustomAccountBuilder) WithMatchingStrategy(requestedCAMatchingStrategy RequestedCAMatchingStrategy) *RequestedWatchlistAdvancedCACheckCustomAccountBuilder { + b.requestedCAMatchingStrategy = requestedCAMatchingStrategy + return b +} + +func (b RequestedWatchlistAdvancedCACheckCustomAccountBuilder) Build() (RequestedWatchlistAdvancedCACustomAccountCheck, error) { + config := RequestedWatchlistAdvancedCACustomAccountConfig{ + RequestedWatchlistAdvancedCAConfig: RequestedWatchlistAdvancedCAConfig{ + Type: constants.WithCustomAccount, + RemoveDeceased: b.removeDeceased, + ShareUrl: b.shareURL, + Sources: b.requestedCASources, + MatchingStrategy: b.requestedCAMatchingStrategy, + }, + APIKey: b.apiKey, + Monitoring: b.monitoring, + Tags: b.tags, + ClientRef: b.clientRef, + } + + return RequestedWatchlistAdvancedCACustomAccountCheck{ + config: config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_custom_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_custom_test.go new file mode 100644 index 0000000..7478ac7 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_custom_test.go @@ -0,0 +1,32 @@ +package check_test + +import ( + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/check" +) + +func ExampleRequestedWatchlistAdvancedCACheckCustomAccountBuilder_Build() { + advancedCACustomAccountCheck, err := check.NewRequestedWatchlistAdvancedCACheckCustomAccountBuilder(). + WithAPIKey("api-key"). + WithMonitoring(true). + WithTags(map[string]string{ + "tag_name": "value", + }). + WithClientRef("client-ref"). + WithMatchingStrategy(check.RequestedExactMatchingStrategy{ExactMatch: true}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := advancedCACustomAccountCheck.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"WATCHLIST_ADVANCED_CA","config":{"type":"WITH_CUSTOM_ACCOUNT","matching_strategy":{"type":"EXACT","exact_match":true},"api_key":"api-key","monitoring":true,"tags":{"tag_name":"value"},"client_ref":"client-ref"}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_matching_strategy.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_matching_strategy.go new file mode 100644 index 0000000..136381b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_matching_strategy.go @@ -0,0 +1,48 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedCAMatchingStrategy is the base type which other CA matching strategies must satisfy +type RequestedCAMatchingStrategy interface { + Type() string +} + +func NewRequestedFuzzyMatchingStrategy() *RequestedFuzzyMatchingStrategy { + return &RequestedFuzzyMatchingStrategy{} +} + +type RequestedFuzzyMatchingStrategy struct { + RequestedCAMatchingStrategy + Fuzziness float64 +} + +// MarshalJSON returns the JSON encoding +func (c RequestedFuzzyMatchingStrategy) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Fuzziness float64 `json:"fuzziness"` + }{ + Type: constants.Fuzzy, + Fuzziness: c.Fuzziness, + }) +} + +type RequestedExactMatchingStrategy struct { + RequestedCAMatchingStrategy + ExactMatch bool +} + +// MarshalJSON returns the JSON encoding +func (c RequestedExactMatchingStrategy) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + ExactMatch bool `json:"exact_match"` + }{ + Type: constants.Exact, + ExactMatch: c.ExactMatch, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_sources.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_sources.go new file mode 100644 index 0000000..cd209f0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_sources.go @@ -0,0 +1,34 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedCASources is the base type which other CA sources must satisfy +type RequestedCASources interface { + Type() string + MarshalJSON() ([]byte, error) +} + +type RequestedTypeListSources struct { + RequestedCASources + Types []string +} + +// Type is the type of the Requested Check +func (c RequestedTypeListSources) Type() string { + return constants.TypeList +} + +// MarshalJSON returns the JSON encoding +func (c RequestedTypeListSources) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Types []string `json:"types"` + }{ + Type: c.Type(), + Types: c.Types, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_yoti.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_yoti.go new file mode 100644 index 0000000..407d9ce --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_yoti.go @@ -0,0 +1,84 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +type RequestedWatchlistAdvancedCAYotiAccountCheck struct { + config RequestedWatchlistAdvancedCAYotiAccountConfig +} + +// Type is the type of the requested check +func (c RequestedWatchlistAdvancedCAYotiAccountCheck) Type() string { + return constants.WatchlistAdvancedCA +} + +// Config is the configuration of the requested check +func (c RequestedWatchlistAdvancedCAYotiAccountCheck) Config() RequestedCheckConfig { + return RequestedCheckConfig(c.config) +} + +// MarshalJSON returns the JSON encoding +func (c RequestedWatchlistAdvancedCAYotiAccountCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} + +type RequestedWatchlistAdvancedCACheckYotiAccountBuilder struct { + removeDeceased bool + shareURL bool + requestedCASources RequestedCASources + requestedCAMatchingStrategy RequestedCAMatchingStrategy +} + +type RequestedWatchlistAdvancedCAYotiAccountConfig struct { + RequestedWatchlistAdvancedCAConfig +} + +// NewRequestedWatchlistAdvancedCACheckYotiAccountBuilder creates a new builder for RequestedWatchlistAdvancedCACheckYotiAccountBuilder +func NewRequestedWatchlistAdvancedCACheckYotiAccountBuilder() *RequestedWatchlistAdvancedCACheckYotiAccountBuilder { + return &RequestedWatchlistAdvancedCACheckYotiAccountBuilder{} +} + +func (b *RequestedWatchlistAdvancedCACheckYotiAccountBuilder) WithRemoveDeceased(removeDeceased bool) *RequestedWatchlistAdvancedCACheckYotiAccountBuilder { + b.removeDeceased = removeDeceased + return b +} + +func (b *RequestedWatchlistAdvancedCACheckYotiAccountBuilder) WithShareURL(shareURL bool) *RequestedWatchlistAdvancedCACheckYotiAccountBuilder { + b.shareURL = shareURL + return b +} + +func (b *RequestedWatchlistAdvancedCACheckYotiAccountBuilder) WithSources(requestedCASources RequestedCASources) *RequestedWatchlistAdvancedCACheckYotiAccountBuilder { + b.requestedCASources = requestedCASources + return b +} + +func (b *RequestedWatchlistAdvancedCACheckYotiAccountBuilder) WithMatchingStrategy(requestedCAMatchingStrategy RequestedCAMatchingStrategy) *RequestedWatchlistAdvancedCACheckYotiAccountBuilder { + b.requestedCAMatchingStrategy = requestedCAMatchingStrategy + return b +} + +func (b RequestedWatchlistAdvancedCACheckYotiAccountBuilder) Build() (RequestedWatchlistAdvancedCAYotiAccountCheck, error) { + config := RequestedWatchlistAdvancedCAYotiAccountConfig{ + RequestedWatchlistAdvancedCAConfig{ + Type: constants.WithYotiAccounts, + RemoveDeceased: b.removeDeceased, + ShareUrl: b.shareURL, + Sources: b.requestedCASources, + MatchingStrategy: b.requestedCAMatchingStrategy, + }, + } + + return RequestedWatchlistAdvancedCAYotiAccountCheck{ + config: config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_yoti_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_yoti_test.go new file mode 100644 index 0000000..112d464 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_advanced_ca_yoti_test.go @@ -0,0 +1,30 @@ +package check_test + +import ( + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/check" +) + +func ExampleNewRequestedWatchlistAdvancedCACheckYotiAccountBuilder() { + advancedCAYotiAccountCheck, err := check.NewRequestedWatchlistAdvancedCACheckYotiAccountBuilder(). + WithRemoveDeceased(true). + WithShareURL(true). + WithSources(check.RequestedTypeListSources{ + Types: []string{"pep", "fitness-probity", "warning"}}). + WithMatchingStrategy(check.RequestedFuzzyMatchingStrategy{Fuzziness: 0.5}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := advancedCAYotiAccountCheck.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"WATCHLIST_ADVANCED_CA","config":{"type":"WITH_YOTI_ACCOUNT","remove_deceased":true,"share_url":true,"sources":{"type":"TYPE_LIST","types":["pep","fitness-probity","warning"]},"matching_strategy":{"type":"FUZZY","fuzziness":0.5}}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_screening.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_screening.go new file mode 100644 index 0000000..9172b37 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_screening.go @@ -0,0 +1,77 @@ +package check + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedWatchlistScreeningCheck requests creation of a Watchlist Screening Check. +// To request a RequestedWatchlistScreeningCheck you must request task.RequestedTextExtractionTask as a minimum +type RequestedWatchlistScreeningCheck struct { + config RequestedWatchlistScreeningCheckConfig +} + +// Type is the type of the requested check +func (c *RequestedWatchlistScreeningCheck) Type() string { + return constants.WatchlistScreening +} + +// Config is the configuration of the requested check +func (c *RequestedWatchlistScreeningCheck) Config() RequestedCheckConfig { + return RequestedCheckConfig(c.config) +} + +// MarshalJSON returns the JSON encoding +func (c *RequestedWatchlistScreeningCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedCheckConfig `json:"config,omitempty"` + }{ + Type: c.Type(), + Config: c.Config(), + }) +} + +// RequestedWatchlistScreeningCheckConfig is the configuration applied when creating +// a watchlist screening check +type RequestedWatchlistScreeningCheckConfig struct { + Categories []string `json:"categories"` +} + +// RequestedWatchlistScreeningCheckBuilder builds a RequestedWatchlistScreeningCheck +type RequestedWatchlistScreeningCheckBuilder struct { + categories []string +} + +// NewRequestedWatchlistScreeningCheckBuilder creates a new builder for RequestedWatchlistScreeningCheck +func NewRequestedWatchlistScreeningCheckBuilder() *RequestedWatchlistScreeningCheckBuilder { + return &RequestedWatchlistScreeningCheckBuilder{} +} + +// WithCategory adds a category to the list of categories used for watchlist screening +func (b *RequestedWatchlistScreeningCheckBuilder) WithCategory(category string) *RequestedWatchlistScreeningCheckBuilder { + b.categories = append(b.categories, category) + return b +} + +// WithAdverseMediaCategory adds ADVERSE_MEDIA to the list of categories used for watchlist screening +func (b *RequestedWatchlistScreeningCheckBuilder) WithAdverseMediaCategory() *RequestedWatchlistScreeningCheckBuilder { + return b.WithCategory(constants.AdverseMedia) +} + +// WithSanctionsCategory adds SANCTIONS to the list of categories used for watchlist screening +func (b *RequestedWatchlistScreeningCheckBuilder) WithSanctionsCategory() *RequestedWatchlistScreeningCheckBuilder { + return b.WithCategory(constants.Sanctions) +} + +// Build builds the RequestedWatchlistScreeningCheck +func (b *RequestedWatchlistScreeningCheckBuilder) Build() (*RequestedWatchlistScreeningCheck, error) { + config := RequestedWatchlistScreeningCheckConfig{ + Categories: b.categories, + } + + return &RequestedWatchlistScreeningCheck{ + config: config, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_screening_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_screening_test.go new file mode 100644 index 0000000..81e8096 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/check/watchlist_screening_test.go @@ -0,0 +1,26 @@ +package check + +import ( + "encoding/json" + "fmt" +) + +func ExampleNewRequestedWatchlistScreeningCheckBuilder() { + watchlistScreeningCheck, err := NewRequestedWatchlistScreeningCheckBuilder(). + WithAdverseMediaCategory(). + WithSanctionsCategory(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(watchlistScreeningCheck) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"WATCHLIST_SCREENING","config":{"categories":["ADVERSE-MEDIA","SANCTIONS"]}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/constants.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/constants.go new file mode 100644 index 0000000..cd69836 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/constants.go @@ -0,0 +1,6 @@ +package create + +const ( + reclassification string = "RECLASSIFICATION" + generic string = "GENERIC" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/create_session_result.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/create_session_result.go new file mode 100644 index 0000000..38b900a --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/create_session_result.go @@ -0,0 +1,8 @@ +package create + +// SessionResult contains the information about a created session +type SessionResult struct { + ClientSessionTokenTTL int `json:"client_session_token_ttl"` + ClientSessionToken string `json:"client_session_token"` + SessionID string `json:"session_id"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/face_capture_resource.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/face_capture_resource.go new file mode 100644 index 0000000..e3f57a1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/face_capture_resource.go @@ -0,0 +1,12 @@ +package facecapture + +type CreateFaceCaptureResourcePayload struct { + RequirementID string `json:"requirement_id"` +} + +// NewCreateFaceCaptureResourcePayload creates a new payload with the given requirement ID. +func NewCreateFaceCaptureResourcePayload(requirementID string) *CreateFaceCaptureResourcePayload { + return &CreateFaceCaptureResourcePayload{ + RequirementID: requirementID, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/face_capture_resource_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/face_capture_resource_test.go new file mode 100644 index 0000000..73992a1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/face_capture_resource_test.go @@ -0,0 +1,15 @@ +package facecapture + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestNewCreateFaceCaptureResourcePayload(t *testing.T) { + requirementID := "test-requirement-id" + payload := NewCreateFaceCaptureResourcePayload(requirementID) + + assert.Assert(t, payload != nil) + assert.Equal(t, payload.RequirementID, requirementID) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/upload_face_capture_image_payload.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/upload_face_capture_image_payload.go new file mode 100644 index 0000000..4fe053e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/upload_face_capture_image_payload.go @@ -0,0 +1,65 @@ +package facecapture + +import ( + "bytes" + "fmt" + "mime/multipart" + "net/textproto" +) + +// UploadFaceCaptureImagePayload represents the payload for uploading a face capture image. +// It includes the image's content type and binary data, and provides methods to prepare +// the payload as a multipart form body. +type UploadFaceCaptureImagePayload struct { + ImageContentType string + ImageContents []byte + + body *bytes.Buffer + writer *multipart.Writer +} + +func NewUploadFaceCaptureImagePayload(contentType string, contents []byte) *UploadFaceCaptureImagePayload { + return &UploadFaceCaptureImagePayload{ + ImageContentType: contentType, + ImageContents: contents, + } +} + +func (p *UploadFaceCaptureImagePayload) Prepare() error { + if p.ImageContentType == "" { + return fmt.Errorf("ImageContentType must not be empty") + } + if len(p.ImageContents) == 0 { + return fmt.Errorf("ImageContents must not be empty") + } + + p.body = &bytes.Buffer{} + p.writer = multipart.NewWriter(p.body) + + header := textproto.MIMEHeader{} + header.Set("Content-Disposition", `form-data; name="binary-content"; filename="face-capture-image"`) + header.Set("Content-Type", p.ImageContentType) + + part, err := p.writer.CreatePart(header) + if err != nil { + return fmt.Errorf("failed to create multipart part: %w", err) + } + + _, err = part.Write(p.ImageContents) + if err != nil { + return fmt.Errorf("failed to write image contents: %w", err) + } + + return p.writer.Close() +} + +func (p *UploadFaceCaptureImagePayload) MultipartFormBody() *bytes.Buffer { + return p.body +} + +// Fixed return type to match SignedRequest expectations +func (p *UploadFaceCaptureImagePayload) Headers() map[string][]string { + return map[string][]string{ + "Content-Type": {p.writer.FormDataContentType()}, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/upload_face_capture_image_payload_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/upload_face_capture_image_payload_test.go new file mode 100644 index 0000000..bf4dd71 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/facecapture/upload_face_capture_image_payload_test.go @@ -0,0 +1,72 @@ +package facecapture + +import ( + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestNewUploadFaceCaptureImagePayload(t *testing.T) { + contentType := "image/png" + content := []byte{1, 2, 3} + + payload := NewUploadFaceCaptureImagePayload(contentType, content) + + assert.Equal(t, payload.ImageContentType, contentType) + assert.DeepEqual(t, payload.ImageContents, content) +} + +func TestPrepare_ValidPayload(t *testing.T) { + contentType := "image/jpeg" + content := []byte{0x01, 0x02, 0x03} + + payload := NewUploadFaceCaptureImagePayload(contentType, content) + err := payload.Prepare() + + assert.NilError(t, err) + assert.Assert(t, payload.body != nil) + assert.Assert(t, payload.writer != nil) + + // Multipart body should contain the image bytes + bodyStr := payload.body.String() + assert.Assert(t, strings.Contains(bodyStr, "Content-Disposition")) + assert.Assert(t, strings.Contains(bodyStr, contentType)) + assert.Assert(t, strings.Contains(bodyStr, string(content))) +} + +func TestPrepare_EmptyContentType(t *testing.T) { + payload := NewUploadFaceCaptureImagePayload("", []byte{1, 2, 3}) + err := payload.Prepare() + + assert.ErrorContains(t, err, "ImageContentType must not be empty") +} + +func TestPrepare_EmptyContents(t *testing.T) { + payload := NewUploadFaceCaptureImagePayload("image/png", []byte{}) + err := payload.Prepare() + + assert.ErrorContains(t, err, "ImageContents must not be empty") +} + +func TestHeaders_ReturnsContentType(t *testing.T) { + payload := NewUploadFaceCaptureImagePayload("image/png", []byte{1, 2, 3}) + err := payload.Prepare() + assert.NilError(t, err) + + headers := payload.Headers() + contentTypes, ok := headers["Content-Type"] + assert.Assert(t, ok) + assert.Assert(t, len(contentTypes) > 0) + assert.Assert(t, strings.HasPrefix(contentTypes[0], "multipart/form-data; boundary=")) +} + +func TestMultipartFormBody_ReturnsBody(t *testing.T) { + payload := NewUploadFaceCaptureImagePayload("image/png", []byte{1, 2, 3}) + err := payload.Prepare() + assert.NilError(t, err) + + body := payload.MultipartFormBody() + assert.Assert(t, body != nil) + assert.Assert(t, body.Len() > 0) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/constants.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/constants.go new file mode 100644 index 0000000..b4ab855 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/constants.go @@ -0,0 +1,12 @@ +package filter + +const ( + includeList string = "WHITELIST" + excludeList string = "BLACKLIST" + + identityDocument string = "ID_DOCUMENT" + supplementaryDocument string = "SUPPLEMENTARY_DOCUMENT" + + orthogonalRestriction string = "ORTHOGONAL_RESTRICTIONS" + documentRestriction string = "DOCUMENT_RESTRICTIONS" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_filter.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_filter.go new file mode 100644 index 0000000..c2c8d2f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_filter.go @@ -0,0 +1,6 @@ +package filter + +// RequestedDocumentFilter filters for a required document, allowing specification of restrictive parameters +type RequestedDocumentFilter interface { + Type() string +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restriction.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restriction.go new file mode 100644 index 0000000..9a9e531 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restriction.go @@ -0,0 +1,41 @@ +package filter + +// RequestedDocumentRestriction represents a document filter for checks and tasks +type RequestedDocumentRestriction struct { + DocumentTypes []string `json:"document_types,omitempty"` + CountryCodes []string `json:"country_codes,omitempty"` +} + +// RequestedDocumentRestrictionBuilder builds a RequestedDocumentRestriction +type RequestedDocumentRestrictionBuilder struct { + documentTypes []string + countryCodes []string +} + +// NewRequestedDocumentRestrictionBuilder creates a new RequestedDocumentRestrictionBuilder +func NewRequestedDocumentRestrictionBuilder() *RequestedDocumentRestrictionBuilder { + return &RequestedDocumentRestrictionBuilder{ + documentTypes: []string{}, + countryCodes: []string{}, + } +} + +// WithCountryCodes sets the country codes of the filter +func (b *RequestedDocumentRestrictionBuilder) WithCountryCodes(countryCodes []string) *RequestedDocumentRestrictionBuilder { + b.countryCodes = countryCodes + return b +} + +// WithDocumentTypes sets the document types of the filter +func (b *RequestedDocumentRestrictionBuilder) WithDocumentTypes(documentTypes []string) *RequestedDocumentRestrictionBuilder { + b.documentTypes = documentTypes + return b +} + +// Build creates a new RequestedDocumentRestriction +func (b *RequestedDocumentRestrictionBuilder) Build() (*RequestedDocumentRestriction, error) { + return &RequestedDocumentRestriction{ + DocumentTypes: b.documentTypes, + CountryCodes: b.countryCodes, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restriction_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restriction_test.go new file mode 100644 index 0000000..67e4122 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restriction_test.go @@ -0,0 +1,64 @@ +package filter + +import ( + "encoding/json" + "fmt" +) + +func ExampleRequestedDocumentRestriction() { + docRestriction, err := NewRequestedDocumentRestrictionBuilder(). + WithDocumentTypes([]string{"PASSPORT"}). + WithCountryCodes([]string{"GBR"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(docRestriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_types":["PASSPORT"],"country_codes":["GBR"]} +} + +func ExampleRequestedDocumentRestrictionBuilder_WithDocumentTypes() { + docRestriction, err := NewRequestedDocumentRestrictionBuilder(). + WithDocumentTypes([]string{"PASSPORT"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(docRestriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"document_types":["PASSPORT"]} +} + +func ExampleRequestedDocumentRestrictionBuilder_WithCountryCodes() { + docRestriction, err := NewRequestedDocumentRestrictionBuilder(). + WithCountryCodes([]string{"GBR"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(docRestriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"country_codes":["GBR"]} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restrictions_filter.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restrictions_filter.go new file mode 100644 index 0000000..ee3b9ef --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restrictions_filter.go @@ -0,0 +1,88 @@ +package filter + +import "encoding/json" + +// RequestedDocumentRestrictionsFilter filters for a required document, allowing specification of restrictive parameters +type RequestedDocumentRestrictionsFilter struct { + inclusion string + documents []*RequestedDocumentRestriction + allowExpiredDocuments *bool + allowNonLatinDocuments *bool +} + +// Type is the type of the document restriction filter +func (r RequestedDocumentRestrictionsFilter) Type() string { + return documentRestriction +} + +// MarshalJSON returns the JSON encoding +func (r *RequestedDocumentRestrictionsFilter) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Inclusion string `json:"inclusion"` + Documents []*RequestedDocumentRestriction `json:"documents"` + AllowExpiredDocuments *bool `json:"allow_expired_documents,omitempty"` + AllowNonLatinDocuments *bool `json:"allow_non_latin_documents,omitempty"` + }{ + Type: r.Type(), + Inclusion: r.inclusion, + Documents: r.documents, + AllowExpiredDocuments: r.allowExpiredDocuments, + AllowNonLatinDocuments: r.allowNonLatinDocuments, + }) +} + +// RequestedDocumentRestrictionsFilterBuilder builds a RequestedDocumentRestrictionsFilter +type RequestedDocumentRestrictionsFilterBuilder struct { + inclusion string + documents []*RequestedDocumentRestriction + allowExpiredDocuments *bool + allowNonLatinDocuments *bool +} + +// NewRequestedDocumentRestrictionsFilterBuilder creates a new RequestedDocumentRestrictionsFilterBuilder +func NewRequestedDocumentRestrictionsFilterBuilder() *RequestedDocumentRestrictionsFilterBuilder { + return &RequestedDocumentRestrictionsFilterBuilder{ + documents: []*RequestedDocumentRestriction{}, + } +} + +// ForIncludeList sets the type restriction to INCLUDE the document restrictions +func (b *RequestedDocumentRestrictionsFilterBuilder) ForIncludeList() *RequestedDocumentRestrictionsFilterBuilder { + b.inclusion = includeList + return b +} + +// ForExcludeList sets the type restriction to EXCLUDE the document restrictions +func (b *RequestedDocumentRestrictionsFilterBuilder) ForExcludeList() *RequestedDocumentRestrictionsFilterBuilder { + b.inclusion = excludeList + return b +} + +// WithDocumentRestriction adds a document restriction to the filter +func (b *RequestedDocumentRestrictionsFilterBuilder) WithDocumentRestriction(docRestriction *RequestedDocumentRestriction) *RequestedDocumentRestrictionsFilterBuilder { + b.documents = append(b.documents, docRestriction) + return b +} + +// WithExpiredDocuments sets a bool value to allowExpiredDocuments on filter +func (b *RequestedDocumentRestrictionsFilterBuilder) WithExpiredDocuments(allowExpiredDocuments bool) *RequestedDocumentRestrictionsFilterBuilder { + b.allowExpiredDocuments = &allowExpiredDocuments + return b +} + +// WithExpiredDocuments sets a bool value to allowExpiredDocuments on filter +func (b *RequestedDocumentRestrictionsFilterBuilder) WithAllowNonLatinDocuments(allowNonLatinDocuments bool) *RequestedDocumentRestrictionsFilterBuilder { + b.allowNonLatinDocuments = &allowNonLatinDocuments + return b +} + +// Build creates a new RequestedDocumentRestrictionsFilter +func (b *RequestedDocumentRestrictionsFilterBuilder) Build() (*RequestedDocumentRestrictionsFilter, error) { + return &RequestedDocumentRestrictionsFilter{ + b.inclusion, + b.documents, + b.allowExpiredDocuments, + b.allowNonLatinDocuments, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restrictions_filter_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restrictions_filter_test.go new file mode 100644 index 0000000..997072c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/document_restrictions_filter_test.go @@ -0,0 +1,140 @@ +package filter + +import ( + "encoding/json" + "fmt" +) + +func ExampleRequestedDocumentRestrictionsFilterBuilder_ForIncludeList() { + docRestriction, err := NewRequestedDocumentRestrictionBuilder(). + WithDocumentTypes([]string{"PASSPORT"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + var docFilter *RequestedDocumentRestrictionsFilter + docFilter, err = NewRequestedDocumentRestrictionsFilterBuilder(). + ForIncludeList(). + WithDocumentRestriction(docRestriction). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(docFilter) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"DOCUMENT_RESTRICTIONS","inclusion":"WHITELIST","documents":[{"document_types":["PASSPORT"]}]} +} + +func ExampleRequestedDocumentRestrictionsFilterBuilder_ForExcludeList() { + docRestriction, err := NewRequestedDocumentRestrictionBuilder(). + WithDocumentTypes([]string{"PASSPORT"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + var docFilter *RequestedDocumentRestrictionsFilter + docFilter, err = NewRequestedDocumentRestrictionsFilterBuilder(). + ForExcludeList(). + WithDocumentRestriction(docRestriction). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(docFilter) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"DOCUMENT_RESTRICTIONS","inclusion":"BLACKLIST","documents":[{"document_types":["PASSPORT"]}]} +} + +func ExampleRequestedDocumentRestrictionsFilterBuilder_withExpiredDocuments() { + restriction, err := NewRequestedDocumentRestrictionsFilterBuilder(). + WithExpiredDocuments(true). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"DOCUMENT_RESTRICTIONS","inclusion":"","documents":[],"allow_expired_documents":true} +} + +func ExampleRequestedDocumentRestrictionsFilterBuilder_withDenyExpiredDocuments() { + restriction, err := NewRequestedDocumentRestrictionsFilterBuilder(). + WithExpiredDocuments(false). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"DOCUMENT_RESTRICTIONS","inclusion":"","documents":[],"allow_expired_documents":false} +} + +func ExampleRequestedDocumentRestrictionsFilterBuilder_withAllowNonLatinDocuments() { + restriction, err := NewRequestedDocumentRestrictionsFilterBuilder(). + WithAllowNonLatinDocuments(true). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"DOCUMENT_RESTRICTIONS","inclusion":"","documents":[],"allow_non_latin_documents":true} +} + +func ExampleRequestedDocumentRestrictionsFilterBuilder_withDenyNonLatinDocuments() { + restriction, err := NewRequestedDocumentRestrictionsFilterBuilder(). + WithAllowNonLatinDocuments(false). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"DOCUMENT_RESTRICTIONS","inclusion":"","documents":[],"allow_non_latin_documents":false} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/orthogonal_restrictions_filter.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/orthogonal_restrictions_filter.go new file mode 100644 index 0000000..2097ffd --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/orthogonal_restrictions_filter.go @@ -0,0 +1,109 @@ +package filter + +import "encoding/json" + +// RequestedOrthogonalRestrictionsFilter filters for a required document, allowing specification of restrictive parameters +type RequestedOrthogonalRestrictionsFilter struct { + countryRestriction *CountryRestriction + typeRestriction *TypeRestriction + allowExpiredDocuments *bool + allowNonLatinDocuments *bool +} + +// Type returns the type of the RequestedOrthogonalRestrictionsFilter +func (r RequestedOrthogonalRestrictionsFilter) Type() string { + return orthogonalRestriction +} + +// MarshalJSON returns the JSON encoding +func (r RequestedOrthogonalRestrictionsFilter) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + CountryRestriction *CountryRestriction `json:"country_restriction,omitempty"` + TypeRestriction *TypeRestriction `json:"type_restriction,omitempty"` + AllowExpiredDocuments *bool `json:"allow_expired_documents,omitempty"` + AllowNonLatinDocuments *bool `json:"allow_non_latin_documents,omitempty"` + }{ + CountryRestriction: r.countryRestriction, + TypeRestriction: r.typeRestriction, + Type: r.Type(), + AllowExpiredDocuments: r.allowExpiredDocuments, + AllowNonLatinDocuments: r.allowNonLatinDocuments, + }) +} + +// RequestedOrthogonalRestrictionsFilterBuilder builds a RequestedOrthogonalRestrictionsFilter +type RequestedOrthogonalRestrictionsFilterBuilder struct { + countryRestriction *CountryRestriction + typeRestriction *TypeRestriction + allowExpiredDocuments *bool + allowNonLatinDocuments *bool +} + +// NewRequestedOrthogonalRestrictionsFilterBuilder creates a new RequestedOrthogonalRestrictionsFilterBuilder +func NewRequestedOrthogonalRestrictionsFilterBuilder() *RequestedOrthogonalRestrictionsFilterBuilder { + return &RequestedOrthogonalRestrictionsFilterBuilder{ + countryRestriction: nil, + typeRestriction: nil, + allowExpiredDocuments: nil, + allowNonLatinDocuments: nil, + } +} + +// WithIncludedCountries sets an "INCLUDE" slice of country codes on the filter +func (b *RequestedOrthogonalRestrictionsFilterBuilder) WithIncludedCountries(countryCodes []string) *RequestedOrthogonalRestrictionsFilterBuilder { + b.countryRestriction = &CountryRestriction{ + includeList, + countryCodes, + } + return b +} + +// WithExcludedCountries sets an "EXCLUDE" slice of country codes on the filter +func (b *RequestedOrthogonalRestrictionsFilterBuilder) WithExcludedCountries(countryCodes []string) *RequestedOrthogonalRestrictionsFilterBuilder { + b.countryRestriction = &CountryRestriction{ + excludeList, + countryCodes, + } + return b +} + +// WithIncludedDocumentTypes sets an "INCLUDE" slice of document types on the filter +func (b *RequestedOrthogonalRestrictionsFilterBuilder) WithIncludedDocumentTypes(documentTypes []string) *RequestedOrthogonalRestrictionsFilterBuilder { + b.typeRestriction = &TypeRestriction{ + includeList, + documentTypes, + } + return b +} + +// WithExcludedDocumentTypes sets an "EXCLUDE" slice of document types on the filter +func (b *RequestedOrthogonalRestrictionsFilterBuilder) WithExcludedDocumentTypes(documentTypes []string) *RequestedOrthogonalRestrictionsFilterBuilder { + b.typeRestriction = &TypeRestriction{ + excludeList, + documentTypes, + } + return b +} + +// WithNonLatinDocuments sets a bool value to allowNonLatinDocuments on filter +func (b *RequestedOrthogonalRestrictionsFilterBuilder) WithNonLatinDocuments(allowNonLatinDocuments bool) *RequestedOrthogonalRestrictionsFilterBuilder { + b.allowNonLatinDocuments = &allowNonLatinDocuments + return b +} + +// WithExpiredDocuments sets a bool value to allowExpiredDocuments on filter +func (b *RequestedOrthogonalRestrictionsFilterBuilder) WithExpiredDocuments(allowExpiredDocuments bool) *RequestedOrthogonalRestrictionsFilterBuilder { + b.allowExpiredDocuments = &allowExpiredDocuments + return b +} + +// Build creates a new RequestedOrthogonalRestrictionsFilter +func (b *RequestedOrthogonalRestrictionsFilterBuilder) Build() (*RequestedOrthogonalRestrictionsFilter, error) { + return &RequestedOrthogonalRestrictionsFilter{ + b.countryRestriction, + b.typeRestriction, + b.allowExpiredDocuments, + b.allowNonLatinDocuments, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/orthogonal_restrictions_filter_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/orthogonal_restrictions_filter_test.go new file mode 100644 index 0000000..5a2d7fe --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/orthogonal_restrictions_filter_test.go @@ -0,0 +1,158 @@ +package filter + +import ( + "encoding/json" + "fmt" +) + +func ExampleRequestedOrthogonalRestrictionsFilterBuilder_WithIncludedCountries() { + restriction, err := NewRequestedOrthogonalRestrictionsFilterBuilder(). + WithIncludedCountries([]string{"KEN", "CIV"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ORTHOGONAL_RESTRICTIONS","country_restriction":{"inclusion":"WHITELIST","country_codes":["KEN","CIV"]}} +} + +func ExampleRequestedOrthogonalRestrictionsFilterBuilder_WithIncludedDocumentTypes() { + restriction, err := NewRequestedOrthogonalRestrictionsFilterBuilder(). + WithIncludedDocumentTypes([]string{"PASSPORT", "DRIVING_LICENCE"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ORTHOGONAL_RESTRICTIONS","type_restriction":{"inclusion":"WHITELIST","document_types":["PASSPORT","DRIVING_LICENCE"]}} +} + +func ExampleRequestedOrthogonalRestrictionsFilterBuilder_WithExcludedDocumentTypes() { + restriction, err := NewRequestedOrthogonalRestrictionsFilterBuilder(). + WithExcludedDocumentTypes([]string{"NATIONAL_ID", "PASS_CARD"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ORTHOGONAL_RESTRICTIONS","type_restriction":{"inclusion":"BLACKLIST","document_types":["NATIONAL_ID","PASS_CARD"]}} +} + +func ExampleRequestedOrthogonalRestrictionsFilterBuilder_WithExcludedCountries() { + restriction, err := NewRequestedOrthogonalRestrictionsFilterBuilder(). + WithExcludedCountries([]string{"CAN", "FJI"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ORTHOGONAL_RESTRICTIONS","country_restriction":{"inclusion":"BLACKLIST","country_codes":["CAN","FJI"]}} +} + +func ExampleRequestedOrthogonalRestrictionsFilterBuilder_withExpiredDocuments() { + restriction, err := NewRequestedOrthogonalRestrictionsFilterBuilder(). + WithExpiredDocuments(true). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ORTHOGONAL_RESTRICTIONS","allow_expired_documents":true} +} + +func ExampleRequestedOrthogonalRestrictionsFilterBuilder_withDenyExpiredDocuments() { + restriction, err := NewRequestedOrthogonalRestrictionsFilterBuilder(). + WithExpiredDocuments(false). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ORTHOGONAL_RESTRICTIONS","allow_expired_documents":false} +} + +func ExampleRequestedOrthogonalRestrictionsFilterBuilder_withNonLatinDocuments() { + restriction, err := NewRequestedOrthogonalRestrictionsFilterBuilder(). + WithNonLatinDocuments(true). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ORTHOGONAL_RESTRICTIONS","allow_non_latin_documents":true} +} + +func ExampleRequestedOrthogonalRestrictionsFilterBuilder_withDenyNonLatinDocuments() { + restriction, err := NewRequestedOrthogonalRestrictionsFilterBuilder(). + WithNonLatinDocuments(false). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(restriction) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ORTHOGONAL_RESTRICTIONS","allow_non_latin_documents":false} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_document.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_document.go new file mode 100644 index 0000000..7d70924 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_document.go @@ -0,0 +1,7 @@ +package filter + +// RequiredDocument is a document to be required for the session +type RequiredDocument interface { + Type() string + MarshalJSON() ([]byte, error) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_id_document.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_id_document.go new file mode 100644 index 0000000..827b20e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_id_document.go @@ -0,0 +1,49 @@ +package filter + +import ( + "encoding/json" +) + +// RequiredIDDocument details a required identity document +type RequiredIDDocument struct { + Filter RequestedDocumentFilter +} + +// Type returns the type of the identity document +func (i *RequiredIDDocument) Type() string { + return identityDocument +} + +// MarshalJSON returns the JSON encoding +func (i *RequiredIDDocument) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Filter RequestedDocumentFilter `json:"filter,omitempty"` + }{ + Type: i.Type(), + Filter: i.Filter, + }) +} + +// NewRequiredIDDocumentBuilder creates a new RequiredIDDocumentBuilder +func NewRequiredIDDocumentBuilder() *RequiredIDDocumentBuilder { + return &RequiredIDDocumentBuilder{} +} + +// RequiredIDDocumentBuilder builds a RequiredIDDocument +type RequiredIDDocumentBuilder struct { + filter RequestedDocumentFilter +} + +// WithFilter sets the filter on the required ID document +func (r RequiredIDDocumentBuilder) WithFilter(filter RequestedDocumentFilter) RequiredIDDocumentBuilder { + r.filter = filter + return r +} + +// Build builds the RequiredIDDocument struct +func (r RequiredIDDocumentBuilder) Build() (*RequiredIDDocument, error) { + return &RequiredIDDocument{ + Filter: r.filter, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_id_document_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_id_document_test.go new file mode 100644 index 0000000..6cab90b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_id_document_test.go @@ -0,0 +1,44 @@ +package filter + +import ( + "encoding/json" + "fmt" +) + +func ExampleRequiredIDDocument_MarshalJSON() { + docRestriction, err := NewRequestedDocumentRestrictionBuilder(). + WithDocumentTypes([]string{"PASSPORT"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + var docFilter *RequestedDocumentRestrictionsFilter + docFilter, err = NewRequestedDocumentRestrictionsFilterBuilder(). + ForIncludeList(). + WithDocumentRestriction(docRestriction). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + var requiredIDDocument *RequiredIDDocument + requiredIDDocument, err = NewRequiredIDDocumentBuilder(). + WithFilter(docFilter). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredIDDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ID_DOCUMENT","filter":{"type":"DOCUMENT_RESTRICTIONS","inclusion":"WHITELIST","documents":[{"document_types":["PASSPORT"]}]}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_supplementary_document.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_supplementary_document.go new file mode 100644 index 0000000..897b2fc --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_supplementary_document.go @@ -0,0 +1,84 @@ +package filter + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/objective" +) + +// RequiredSupplementaryDocument details a required supplementary document +type RequiredSupplementaryDocument struct { + Filter RequestedDocumentFilter + DocumentTypes []string + CountryCodes []string + Objective objective.Objective +} + +// Type returns the type of the supplementary document +func (r *RequiredSupplementaryDocument) Type() string { + return supplementaryDocument +} + +// MarshalJSON returns the JSON encoding +func (r *RequiredSupplementaryDocument) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Filter RequestedDocumentFilter `json:"filter,omitempty"` + CountryCodes []string `json:"country_codes,omitempty"` + DocumentTypes []string `json:"document_types,omitempty"` + Objective objective.Objective `json:"objective,omitempty"` + }{ + Type: r.Type(), + Filter: r.Filter, + CountryCodes: r.CountryCodes, + DocumentTypes: r.DocumentTypes, + Objective: r.Objective, + }) +} + +// NewRequiredSupplementaryDocumentBuilder creates a new RequiredSupplementaryDocumentBuilder +func NewRequiredSupplementaryDocumentBuilder() *RequiredSupplementaryDocumentBuilder { + return &RequiredSupplementaryDocumentBuilder{} +} + +// RequiredSupplementaryDocumentBuilder builds a RequiredSupplementaryDocument +type RequiredSupplementaryDocumentBuilder struct { + filter RequestedDocumentFilter + documentTypes []string + countryCodes []string + objective objective.Objective +} + +// WithFilter sets the filter on the required supplementary document +func (r *RequiredSupplementaryDocumentBuilder) WithFilter(filter RequestedDocumentFilter) *RequiredSupplementaryDocumentBuilder { + r.filter = filter + return r +} + +// WithCountryCodes sets the country codes on the required supplementary document +func (r *RequiredSupplementaryDocumentBuilder) WithCountryCodes(countryCodes []string) *RequiredSupplementaryDocumentBuilder { + r.countryCodes = countryCodes + return r +} + +// WithDocumentTypes sets the document types on the required supplementary document +func (r *RequiredSupplementaryDocumentBuilder) WithDocumentTypes(documentTypes []string) *RequiredSupplementaryDocumentBuilder { + r.documentTypes = documentTypes + return r +} + +// WithObjective sets the objective for the required supplementary document +func (r *RequiredSupplementaryDocumentBuilder) WithObjective(objective objective.Objective) *RequiredSupplementaryDocumentBuilder { + r.objective = objective + return r +} + +// Build builds the RequiredSupplementaryDocument struct +func (r *RequiredSupplementaryDocumentBuilder) Build() (*RequiredSupplementaryDocument, error) { + return &RequiredSupplementaryDocument{ + Filter: r.filter, + DocumentTypes: r.documentTypes, + CountryCodes: r.countryCodes, + Objective: r.objective, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_supplementary_document_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_supplementary_document_test.go new file mode 100644 index 0000000..d50b95c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/required_supplementary_document_test.go @@ -0,0 +1,207 @@ +package filter + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/objective" +) + +func ExampleRequiredSupplementaryDocument() { + var requiredSupplementaryDocument *RequiredSupplementaryDocument + requiredSupplementaryDocument, err := NewRequiredSupplementaryDocumentBuilder(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredSupplementaryDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT"} +} + +func ExampleRequiredSupplementaryDocumentBuilder_WithFilter() { + docRestriction, err := NewRequestedDocumentRestrictionBuilder(). + WithDocumentTypes([]string{"UTILITY_BILL"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + var docFilter *RequestedDocumentRestrictionsFilter + docFilter, err = NewRequestedDocumentRestrictionsFilterBuilder(). + ForIncludeList(). + WithDocumentRestriction(docRestriction). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + var requiredSupplementaryDocument *RequiredSupplementaryDocument + requiredSupplementaryDocument, err = NewRequiredSupplementaryDocumentBuilder(). + WithFilter(docFilter). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredSupplementaryDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT","filter":{"type":"DOCUMENT_RESTRICTIONS","inclusion":"WHITELIST","documents":[{"document_types":["UTILITY_BILL"]}]}} +} + +func ExampleRequiredSupplementaryDocumentBuilder_WithCountryCodes() { + var requiredSupplementaryDocument *RequiredSupplementaryDocument + requiredSupplementaryDocument, err := NewRequiredSupplementaryDocumentBuilder(). + WithCountryCodes([]string{"SOME_COUNTRY"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredSupplementaryDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT","country_codes":["SOME_COUNTRY"]} +} + +func ExampleRequiredSupplementaryDocumentBuilder_WithCountryCodes_empty() { + var requiredSupplementaryDocument *RequiredSupplementaryDocument + requiredSupplementaryDocument, err := NewRequiredSupplementaryDocumentBuilder(). + WithCountryCodes([]string{}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredSupplementaryDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT"} +} + +func ExampleRequiredSupplementaryDocumentBuilder_WithDocumentTypes() { + var requiredSupplementaryDocument *RequiredSupplementaryDocument + requiredSupplementaryDocument, err := NewRequiredSupplementaryDocumentBuilder(). + WithDocumentTypes([]string{"SOME_DOCUMENT_TYPE"}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredSupplementaryDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT","document_types":["SOME_DOCUMENT_TYPE"]} +} + +func ExampleRequiredSupplementaryDocumentBuilder_WithDocumentTypes_empty() { + var requiredSupplementaryDocument *RequiredSupplementaryDocument + requiredSupplementaryDocument, err := NewRequiredSupplementaryDocumentBuilder(). + WithDocumentTypes([]string{}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredSupplementaryDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT"} +} + +func ExampleRequiredSupplementaryDocumentBuilder_WithObjective() { + var requiredSupplementaryDocument *RequiredSupplementaryDocument + requiredSupplementaryDocument, err := NewRequiredSupplementaryDocumentBuilder(). + WithObjective(&mockObjective{}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredSupplementaryDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT","objective":{"type":"SOME_OBJECTIVE"}} +} + +func ExampleRequiredSupplementaryDocumentBuilder_WithObjective_proofOfAddress() { + var proofOfAddress objective.Objective + proofOfAddress, err := objective.NewProofOfAddressObjectiveBuilder().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + var requiredSupplementaryDocument *RequiredSupplementaryDocument + requiredSupplementaryDocument, err = NewRequiredSupplementaryDocumentBuilder(). + WithObjective(proofOfAddress). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(requiredSupplementaryDocument) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT","objective":{"type":"PROOF_OF_ADDRESS"}} +} + +type mockObjective struct{} + +func (o *mockObjective) Type() string { + return "SOME_OBJECTIVE" +} + +// MarshalJSON returns the JSON encoding +func (o *mockObjective) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + }{ + Type: o.Type(), + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/restriction.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/restriction.go new file mode 100644 index 0000000..7709f16 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/filter/restriction.go @@ -0,0 +1,13 @@ +package filter + +// TypeRestriction is a restriction of the type of document required +type TypeRestriction struct { + Inclusion string `json:"inclusion"` + DocumentTypes []string `json:"document_types"` +} + +// CountryRestriction is a restriction of the country in which a document pertains to +type CountryRestriction struct { + Inclusion string `json:"inclusion"` + CountryCodes []string `json:"country_codes"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/import_token.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/import_token.go new file mode 100644 index 0000000..33ccb42 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/import_token.go @@ -0,0 +1,35 @@ +package create + +import "time" + +type ImportToken struct { + Ttl int `json:"ttl"` +} + +// defaultImportTokenTTL is 12 Months +const defaultImportTokenTTL = time.Hour * 24 * 365 + +// NewImportTokenBuilder creates a new ImportTokenBuilder +func NewImportTokenBuilder() *ImportTokenBuilder { + return &ImportTokenBuilder{ + Ttl: int(defaultImportTokenTTL.Seconds()), + } +} + +// ImportTokenBuilder builds the ImportToken struct +type ImportTokenBuilder struct { + Ttl int +} + +// WithTTL sets the TTL of the import-token (in seconds) +func (b *ImportTokenBuilder) WithTTL(ttl int) *ImportTokenBuilder { + b.Ttl = ttl + return b +} + +// Build builds the ImportToken struct using the supplied values +func (b *ImportTokenBuilder) Build() (*ImportToken, error) { + return &ImportToken{ + Ttl: b.Ttl, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/import_token_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/import_token_test.go new file mode 100644 index 0000000..fdf3384 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/import_token_test.go @@ -0,0 +1,28 @@ +package create + +import ( + "encoding/json" + "fmt" + "time" +) + +func ExampleImportTokenBuilder_Build() { + ttl := time.Hour * 24 * 30 + it, err := NewImportTokenBuilder(). + WithTTL(int(ttl.Seconds())). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(it) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"ttl":2592000} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/notification_config.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/notification_config.go new file mode 100644 index 0000000..9391a1f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/notification_config.go @@ -0,0 +1,75 @@ +package create + +import "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" + +// NotificationConfig represents the configuration properties for notifications within the Doc Scan (IDV) system. +// Notifications can be configured within a Doc Scan (IDV) session to allow your backend to be +// notified of certain events, without having to constantly poll for the state of a session. +type NotificationConfig struct { + AuthToken string `json:"auth_token,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + Topics []string `json:"topics,omitempty"` +} + +// NewNotificationConfigBuilder creates a new NotificationConfigBuilder +func NewNotificationConfigBuilder() *NotificationConfigBuilder { + return &NotificationConfigBuilder{} +} + +// NotificationConfigBuilder builds the NotificationConfig struct +type NotificationConfigBuilder struct { + authToken string + endpoint string + topics []string +} + +// WithAuthToken sets the authorization token to be included in call-back messages +func (b *NotificationConfigBuilder) WithAuthToken(token string) *NotificationConfigBuilder { + b.authToken = token + return b +} + +// WithEndpoint sets the endpoint that notifications should be sent to +func (b *NotificationConfigBuilder) WithEndpoint(endpoint string) *NotificationConfigBuilder { + b.endpoint = endpoint + return b +} + +// WithTopic adds a topic to the slice of topics that trigger notification messages +func (b *NotificationConfigBuilder) WithTopic(topic string) *NotificationConfigBuilder { + b.topics = append(b.topics, topic) + return b +} + +// ForResourceUpdate Adds "RESOURCE_UPDATE" to the slice of topics that trigger notification messages +func (b *NotificationConfigBuilder) ForResourceUpdate() *NotificationConfigBuilder { + b.topics = append(b.topics, constants.ResourceUpdate) + return b +} + +// ForTaskCompletion Adds "TASK_COMPLETION" to the slice of topics that trigger notification messages +func (b *NotificationConfigBuilder) ForTaskCompletion() *NotificationConfigBuilder { + b.topics = append(b.topics, constants.TaskCompletion) + return b +} + +// ForSessionCompletion Adds "SESSION_COMPLETION" to the slice of topics that trigger notification messages +func (b *NotificationConfigBuilder) ForSessionCompletion() *NotificationConfigBuilder { + b.topics = append(b.topics, constants.SessionCompletion) + return b +} + +// ForCheckCompletion Adds "CHECK_COMPLETION" to the slice of topics that trigger notification messages +func (b *NotificationConfigBuilder) ForCheckCompletion() *NotificationConfigBuilder { + b.topics = append(b.topics, constants.CheckCompletion) + return b +} + +// Build builds the NotificationConfig struct using the supplied values +func (b *NotificationConfigBuilder) Build() (*NotificationConfig, error) { + return &NotificationConfig{ + b.authToken, + b.endpoint, + b.topics, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/notification_config_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/notification_config_test.go new file mode 100644 index 0000000..ce4a214 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/notification_config_test.go @@ -0,0 +1,32 @@ +package create + +import ( + "encoding/json" + "fmt" +) + +func ExampleNotificationConfigBuilder_Build() { + notifications, err := NewNotificationConfigBuilder(). + WithAuthToken("auth-token"). + WithEndpoint("/endpoint"). + WithTopic("SOME_TOPIC"). + ForCheckCompletion(). + ForResourceUpdate(). + ForSessionCompletion(). + ForTaskCompletion(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(notifications) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"auth_token":"auth-token","endpoint":"/endpoint","topics":["SOME_TOPIC","CHECK_COMPLETION","RESOURCE_UPDATE","SESSION_COMPLETION","TASK_COMPLETION"]} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/objective.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/objective.go new file mode 100644 index 0000000..31c4f5e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/objective.go @@ -0,0 +1,6 @@ +package objective + +type Objective interface { + Type() string + MarshalJSON() ([]byte, error) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/proof_of_address.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/proof_of_address.go new file mode 100644 index 0000000..5374bb8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/proof_of_address.go @@ -0,0 +1,39 @@ +package objective + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// ProofOfAddressObjective requests creation of a proof of address objective +type ProofOfAddressObjective struct { +} + +// Type is the objective type +func (o *ProofOfAddressObjective) Type() string { + return constants.ProofOfAddress +} + +// MarshalJSON returns the JSON encoding +func (o *ProofOfAddressObjective) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + }{ + Type: o.Type(), + }) +} + +// NewProofOfAddressObjectiveBuilder creates a new ProofOfAddressObjectiveBuilder +func NewProofOfAddressObjectiveBuilder() *ProofOfAddressObjectiveBuilder { + return &ProofOfAddressObjectiveBuilder{} +} + +// ProofOfAddressObjectiveBuilder builds a ProofOfAddress +type ProofOfAddressObjectiveBuilder struct { +} + +// Build builds the ProofOfAddressObjective +func (builder *ProofOfAddressObjectiveBuilder) Build() (*ProofOfAddressObjective, error) { + return &ProofOfAddressObjective{}, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/proof_of_address_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/proof_of_address_test.go new file mode 100644 index 0000000..3456c5d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/objective/proof_of_address_test.go @@ -0,0 +1,24 @@ +package objective + +import ( + "encoding/json" + "fmt" +) + +func ExampleProofOfAddressObjectiveBuilder() { + objective, err := NewProofOfAddressObjectiveBuilder(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(objective) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"PROOF_OF_ADDRESS"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/sdk_config.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/sdk_config.go new file mode 100644 index 0000000..fbbe596 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/sdk_config.go @@ -0,0 +1,206 @@ +package create + +import "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" + +// SDKConfig provides configuration properties for the the web/native clients +type SDKConfig struct { + AllowedCaptureMethods string `json:"allowed_capture_methods,omitempty"` + PrimaryColour string `json:"primary_colour,omitempty"` + SecondaryColour string `json:"secondary_colour,omitempty"` + FontColour string `json:"font_colour,omitempty"` + Locale string `json:"locale,omitempty"` + PresetIssuingCountry string `json:"preset_issuing_country,omitempty"` + SuccessUrl string `json:"success_url,omitempty"` + ErrorUrl string `json:"error_url,omitempty"` + PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` + AttemptsConfiguration *AttemptsConfiguration `json:"attempts_configuration,omitempty"` + AllowHandOff bool `json:"allow_handoff,omitempty"` + DarkMode string `json:"dark_mode,omitempty"` + PrimaryColourDarkMode string `json:"primary_colour_dark_mode,omitempty"` + BiometricConsentFlow string `json:"biometric_consent_flow,omitempty"` + BrandId string `json:"brand_id,omitempty"` +} + +type AttemptsConfiguration struct { + IdDocumentTextDataExtraction map[string]int `json:"ID_DOCUMENT_TEXT_DATA_EXTRACTION,omitempty"` +} + +// NewSdkConfigBuilder creates a new SdkConfigBuilder +func NewSdkConfigBuilder() *SdkConfigBuilder { + return &SdkConfigBuilder{} +} + +// SdkConfigBuilder builds the SDKConfig struct +type SdkConfigBuilder struct { + allowedCaptureMethods string + primaryColour string + secondaryColour string + fontColour string + locale string + presetIssuingCountry string + successUrl string + errorUrl string + privacyPolicyUrl string + idDocumentTextDataExtractionAttempts map[string]int + allowHandOff bool + darkMode string + primaryColourDarkMode string + biometricConsentFlow string + brandId string +} + +// WithAllowedCaptureMethods sets the allowed capture methods on the builder +func (b *SdkConfigBuilder) WithAllowedCaptureMethods(captureMethods string) *SdkConfigBuilder { + b.allowedCaptureMethods = captureMethods + return b +} + +// WithAllowsCamera sets the allowed capture method to "CAMERA" +func (b *SdkConfigBuilder) WithAllowsCamera() *SdkConfigBuilder { + return b.WithAllowedCaptureMethods(constants.Camera) +} + +// WithAllowsCameraAndUpload sets the allowed capture method to "CAMERA_AND_UPLOAD" +func (b *SdkConfigBuilder) WithAllowsCameraAndUpload() *SdkConfigBuilder { + return b.WithAllowedCaptureMethods(constants.CameraAndUpload) +} + +// WithPrimaryColour sets the primary colour to be used by the web/native client, hexadecimal value e.g. #ff0000 +func (b *SdkConfigBuilder) WithPrimaryColour(colour string) *SdkConfigBuilder { + b.primaryColour = colour + return b +} + +// WithSecondaryColour sets the secondary colour to be used by the web/native client (used on the button), hexadecimal value e.g. #ff0000 +func (b *SdkConfigBuilder) WithSecondaryColour(colour string) *SdkConfigBuilder { + b.secondaryColour = colour + return b +} + +// WithFontColour the font colour to be used by the web/native client (used on the button), hexadecimal value e.g. #ff0000 +func (b *SdkConfigBuilder) WithFontColour(colour string) *SdkConfigBuilder { + b.fontColour = colour + return b +} + +// WithLocale sets the language locale use by the web/native client +func (b *SdkConfigBuilder) WithLocale(locale string) *SdkConfigBuilder { + b.locale = locale + return b +} + +// WithPresetIssuingCountry sets the preset issuing country used by the web/native client +func (b *SdkConfigBuilder) WithPresetIssuingCountry(country string) *SdkConfigBuilder { + b.presetIssuingCountry = country + return b +} + +// WithSuccessUrl sets the success URL for the redirect that follows the web/native client uploading documents successfully +func (b *SdkConfigBuilder) WithSuccessUrl(url string) *SdkConfigBuilder { + b.successUrl = url + return b +} + +// WithErrorUrl sets the error URL for the redirect that follows the web/native client uploading documents unsuccessfully +func (b *SdkConfigBuilder) WithErrorUrl(url string) *SdkConfigBuilder { + b.errorUrl = url + return b +} + +// WithPrivacyPolicyUrl sets the privacy policy URL +func (b *SdkConfigBuilder) WithPrivacyPolicyUrl(url string) *SdkConfigBuilder { + b.privacyPolicyUrl = url + return b +} + +func (b *SdkConfigBuilder) WithIdDocumentTextExtractionCategoryAttempts(category string, attempts int) *SdkConfigBuilder { + if b.idDocumentTextDataExtractionAttempts == nil { + b.idDocumentTextDataExtractionAttempts = make(map[string]int) + } + b.idDocumentTextDataExtractionAttempts[category] = attempts + return b +} + +func (b *SdkConfigBuilder) WithIdDocumentTextExtractionReclassificationAttempts(attempts int) *SdkConfigBuilder { + return b.WithIdDocumentTextExtractionCategoryAttempts(reclassification, attempts) +} + +func (b *SdkConfigBuilder) WithIdDocumentTextExtractionGenericAttempts(attempts int) *SdkConfigBuilder { + return b.WithIdDocumentTextExtractionCategoryAttempts(generic, attempts) +} + +func (b *SdkConfigBuilder) WithAllowHandOff(allowHandOff bool) *SdkConfigBuilder { + b.allowHandOff = allowHandOff + return b +} + +func (b *SdkConfigBuilder) WithDarkMode(darkMode string) *SdkConfigBuilder { + b.darkMode = darkMode + return b +} + +func (b *SdkConfigBuilder) WithDarkModeOn() *SdkConfigBuilder { + b.darkMode = "ON" + return b +} + +func (b *SdkConfigBuilder) WithDarkModeOff() *SdkConfigBuilder { + b.darkMode = "OFF" + return b +} + +func (b *SdkConfigBuilder) WithDarkModeAuto() *SdkConfigBuilder { + b.darkMode = "AUTO" + return b +} + +func (b *SdkConfigBuilder) WithPrimaryColourDarkMode(colour string) *SdkConfigBuilder { + b.primaryColourDarkMode = colour + return b +} + +func (b *SdkConfigBuilder) WithBiometricConsentFlow(biometricConsentFlow string) *SdkConfigBuilder { + b.biometricConsentFlow = biometricConsentFlow + return b +} + +func (b *SdkConfigBuilder) WithEarlyBiometricConsentFlow() *SdkConfigBuilder { + return b.WithBiometricConsentFlow(constants.Early) +} + +func (b *SdkConfigBuilder) WithJustInTimeBiometricConsentFlow() *SdkConfigBuilder { + return b.WithBiometricConsentFlow(constants.JustInTime) +} + +func (b *SdkConfigBuilder) WithBrandId(brandId string) *SdkConfigBuilder { + b.brandId = brandId + return b +} + +// Build builds the SDKConfig struct using the supplied values +func (b *SdkConfigBuilder) Build() (*SDKConfig, error) { + sdkConf := &SDKConfig{ + b.allowedCaptureMethods, + b.primaryColour, + b.secondaryColour, + b.fontColour, + b.locale, + b.presetIssuingCountry, + b.successUrl, + b.errorUrl, + b.privacyPolicyUrl, + nil, + b.allowHandOff, + b.darkMode, + b.primaryColourDarkMode, + b.biometricConsentFlow, + b.brandId, + } + + if b.idDocumentTextDataExtractionAttempts != nil { + sdkConf.AttemptsConfiguration = &AttemptsConfiguration{ + IdDocumentTextDataExtraction: b.idDocumentTextDataExtractionAttempts, + } + } + return sdkConf, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/sdk_config_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/sdk_config_test.go new file mode 100644 index 0000000..b917290 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/sdk_config_test.go @@ -0,0 +1,259 @@ +package create + +import ( + "encoding/json" + "fmt" +) + +func ExampleSdkConfigBuilder_Build() { + sdkConfig, err := NewSdkConfigBuilder(). + WithAllowsCamera(). + WithErrorUrl("https://example.com/error"). + WithFontColour("#ff0000"). + WithLocale("fr_FR"). + WithPresetIssuingCountry("USA"). + WithPrimaryColour("#aa1111"). + WithSecondaryColour("#bb2222"). + WithSuccessUrl("https://example.com/success"). + WithPrivacyPolicyUrl("https://example.com/privacy"). + WithIdDocumentTextExtractionCategoryAttempts("test_category", 3). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"allowed_capture_methods":"CAMERA","primary_colour":"#aa1111","secondary_colour":"#bb2222","font_colour":"#ff0000","locale":"fr_FR","preset_issuing_country":"USA","success_url":"https://example.com/success","error_url":"https://example.com/error","privacy_policy_url":"https://example.com/privacy","attempts_configuration":{"ID_DOCUMENT_TEXT_DATA_EXTRACTION":{"test_category":3}}} +} + +func ExampleSdkConfigBuilder_Build_repeatedCallWithIdDocumentTextExtractionCategoryAttempts() { + sdkConfig, err := NewSdkConfigBuilder(). + WithIdDocumentTextExtractionCategoryAttempts("test_category", 3). + WithIdDocumentTextExtractionCategoryAttempts("test_category", 2). + WithIdDocumentTextExtractionCategoryAttempts("test_category", 1). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"attempts_configuration":{"ID_DOCUMENT_TEXT_DATA_EXTRACTION":{"test_category":1}}} +} + +func ExampleSdkConfigBuilder_Build_multipleCategoriesWithIdDocumentTextExtractionCategoryAttempts() { + sdkConfig, err := NewSdkConfigBuilder(). + WithIdDocumentTextExtractionGenericAttempts(3). + WithIdDocumentTextExtractionCategoryAttempts("test_category", 2). + WithIdDocumentTextExtractionReclassificationAttempts(1). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"attempts_configuration":{"ID_DOCUMENT_TEXT_DATA_EXTRACTION":{"GENERIC":3,"RECLASSIFICATION":1,"test_category":2}}} +} + +func ExampleSdkConfigBuilder_WithAllowsCameraAndUpload() { + sdkConfig, err := NewSdkConfigBuilder(). + WithAllowsCameraAndUpload(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"allowed_capture_methods":"CAMERA_AND_UPLOAD"} +} + +func ExampleSdkConfigBuilder_WithEarlyBiometricConsentFlow() { + sdkConfig, err := NewSdkConfigBuilder(). + WithEarlyBiometricConsentFlow(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"biometric_consent_flow":"EARLY"} +} + +func ExampleSdkConfigBuilder_WithJustInTimeBiometricConsentFlow() { + sdkConfig, err := NewSdkConfigBuilder(). + WithJustInTimeBiometricConsentFlow(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"biometric_consent_flow":"JUST_IN_TIME"} +} + +func ExampleSdkConfigBuilder_WithAllowHandOff() { + sdkConfig, err := NewSdkConfigBuilder(). + WithAllowHandOff(true). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"allow_handoff":true} +} + +func ExampleSdkConfigBuilder_WithDarkMode() { + sdkConfig, err := NewSdkConfigBuilder(). + WithDarkMode("ON"). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"dark_mode":"ON"} +} + +func ExampleSdkConfigBuilder_WithBrandId() { + sdkConfig, err := NewSdkConfigBuilder(). + WithBrandId("some_brand_id"). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"brand_id":"some_brand_id"} +} + +func ExampleSdkConfigBuilder_WithDarkModeOff() { + sdkConfig, err := NewSdkConfigBuilder(). + WithDarkModeOff(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"dark_mode":"OFF"} +} + +func ExampleSdkConfigBuilder_WithDarkModeAuto() { + sdkConfig, err := NewSdkConfigBuilder(). + WithDarkModeAuto(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"dark_mode":"AUTO"} +} + +func ExampleSdkConfigBuilder_WithPrimaryColourDarkMode() { + sdkConfig, err := NewSdkConfigBuilder(). + WithPrimaryColourDarkMode("SOME_COLOUR"). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sdkConfig) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"primary_colour_dark_mode":"SOME_COLOUR"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/session_spec.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/session_spec.go new file mode 100644 index 0000000..10fc148 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/session_spec.go @@ -0,0 +1,195 @@ +package create + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/check" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/filter" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/task" +) + +// SessionSpecification is the definition for the Doc Scan (IDV) Session to be created +type SessionSpecification struct { + // ClientSessionTokenTTL Client-session-token time-to-live to apply to the created Session + ClientSessionTokenTTL int `json:"client_session_token_ttl,omitempty"` + + // ResourcesTTL time-to-live used for all Resources created in the course of the session + ResourcesTTL int `json:"resources_ttl,omitempty"` + + // UserTrackingID the User tracking ID, used to track returning users + UserTrackingID string `json:"user_tracking_id,omitempty"` + + // Notifications for configuring call-back messages + Notifications *NotificationConfig `json:"notifications,omitempty"` + + // RequestedChecks is a slice of check.RequestedCheck objects defining the Checks to be performed on each Document + RequestedChecks []check.RequestedCheck `json:"requested_checks,omitempty"` + + // RequestedTasks is a slice of task.RequestedTask objects defining the Tasks to be performed on each Document + RequestedTasks []task.RequestedTask `json:"requested_tasks,omitempty"` + + // SdkConfig retrieves the SDK configuration set of the session specification + SdkConfig *SDKConfig `json:"sdk_config,omitempty"` + + // RequiredDocuments is a slice of documents that are required from the user to satisfy a sessions requirements. + RequiredDocuments []filter.RequiredDocument `json:"required_documents,omitempty"` + + // BlockBiometricConsent sets whether or not to block the collection of biometric consent + BlockBiometricConsent *bool `json:"block_biometric_consent,omitempty"` + + // IdentityProfileRequirements is a JSON object for defining a required identity profile + // within the scope of a trust framework and scheme. + IdentityProfileRequirements *json.RawMessage `json:"identity_profile_requirements,omitempty"` + + // AdvancedIdentityProfileRequirements is a JSON object for defining a required advanced identity profile + // within the scope of specified trust frameworks and schemes. + AdvancedIdentityProfileRequirements *json.RawMessage `json:"advanced_identity_profile_requirements,omitempty"` + + // CreateIdentityProfilePreview is a bool for enabling the creation of the IdentityProfilePreview + CreateIdentityProfilePreview bool `json:"create_identity_profile_preview,omitempty"` + + // Subject provides information on the subject allowing to track the same user across multiple sessions. + // Should not contain any personal identifiable information. + Subject *json.RawMessage `json:"subject,omitempty"` + + // ImportToken requests the creation of an import_token. + ImportToken *ImportToken `json:"import_token,omitempty"` + + //Ephemeral Media to set ephemeral or not + EphemeralMedia *bool `json:"ephemeral_media,omitempty"` +} + +// SessionSpecificationBuilder builds the SessionSpecification struct +type SessionSpecificationBuilder struct { + clientSessionTokenTTL int + resourcesTTL int + userTrackingID string + notifications *NotificationConfig + requestedChecks []check.RequestedCheck + requestedTasks []task.RequestedTask + sdkConfig *SDKConfig + requiredDocuments []filter.RequiredDocument + blockBiometricConsent *bool + identityProfileRequirements *json.RawMessage + advancedIdentityProfileRequirements *json.RawMessage + createIdentityProfilePreview bool + subject *json.RawMessage + importToken *ImportToken + ephemeralMedia *bool +} + +// NewSessionSpecificationBuilder creates a new SessionSpecificationBuilder +func NewSessionSpecificationBuilder() *SessionSpecificationBuilder { + return &SessionSpecificationBuilder{} +} + +// WithClientSessionTokenTTL sets the client session token TTL (time-to-live) +func (b *SessionSpecificationBuilder) WithClientSessionTokenTTL(clientSessionTokenTTL int) *SessionSpecificationBuilder { + b.clientSessionTokenTTL = clientSessionTokenTTL + return b +} + +// WithResourcesTTL sets the client session token TTL (time-to-live) +func (b *SessionSpecificationBuilder) WithResourcesTTL(resourcesTTL int) *SessionSpecificationBuilder { + b.resourcesTTL = resourcesTTL + return b +} + +// WithUserTrackingID sets the user tracking ID +func (b *SessionSpecificationBuilder) WithUserTrackingID(userTrackingID string) *SessionSpecificationBuilder { + b.userTrackingID = userTrackingID + return b +} + +// WithNotifications sets the NotificationConfig +func (b *SessionSpecificationBuilder) WithNotifications(notificationConfig *NotificationConfig) *SessionSpecificationBuilder { + b.notifications = notificationConfig + return b +} + +// WithRequestedCheck adds a RequestedCheck to the required checks +func (b *SessionSpecificationBuilder) WithRequestedCheck(requestedCheck check.RequestedCheck) *SessionSpecificationBuilder { + b.requestedChecks = append(b.requestedChecks, requestedCheck) + return b +} + +// WithRequestedTask adds a RequestedTask to the required tasks +func (b *SessionSpecificationBuilder) WithRequestedTask(requestedTask task.RequestedTask) *SessionSpecificationBuilder { + b.requestedTasks = append(b.requestedTasks, requestedTask) + return b +} + +// WithSDKConfig sets the SDKConfig +func (b *SessionSpecificationBuilder) WithSDKConfig(SDKConfig *SDKConfig) *SessionSpecificationBuilder { + b.sdkConfig = SDKConfig + return b +} + +// WithRequiredDocument adds a required document to the session specification +func (b *SessionSpecificationBuilder) WithRequiredDocument(document filter.RequiredDocument) *SessionSpecificationBuilder { + b.requiredDocuments = append(b.requiredDocuments, document) + return b +} + +// WithBlockBiometricConsent sets whether or not to block the collection of biometric consent +func (b *SessionSpecificationBuilder) WithBlockBiometricConsent(blockBiometricConsent bool) *SessionSpecificationBuilder { + b.blockBiometricConsent = &blockBiometricConsent + return b +} + +// WithCreateIdentityProfilePreview sets whether or not an Identity Profile Preview will be created. +func (b *SessionSpecificationBuilder) WithCreateIdentityProfilePreview(createIdentityProfilePreview bool) *SessionSpecificationBuilder { + b.createIdentityProfilePreview = createIdentityProfilePreview + return b +} + +// WithIdentityProfileRequirements adds Identity Profile Requirements to the session. Must be valid JSON. +func (b *SessionSpecificationBuilder) WithIdentityProfileRequirements(identityProfile json.RawMessage) *SessionSpecificationBuilder { + b.identityProfileRequirements = &identityProfile + return b +} + +// WithAdvancedIdentityProfileRequirements adds Advanced Identity Profile Requirements to the session. Must be valid JSON. +func (b *SessionSpecificationBuilder) WithAdvancedIdentityProfileRequirements(advancedIdentityProfile json.RawMessage) *SessionSpecificationBuilder { + b.advancedIdentityProfileRequirements = &advancedIdentityProfile + return b +} + +// WithSubject adds Subject to the session. Must be valid JSON. +func (b *SessionSpecificationBuilder) WithSubject(subject json.RawMessage) *SessionSpecificationBuilder { + b.subject = &subject + return b +} + +// WithImportToken sets whether an ImportToken is to be generated. +func (b *SessionSpecificationBuilder) WithImportToken(importToken *ImportToken) *SessionSpecificationBuilder { + b.importToken = importToken + return b +} + +// WithEphemeralMedia sets whether to block or not to block the collection of ephemeral media +func (b *SessionSpecificationBuilder) WithEphemeralMedia(ephemeralMedia bool) *SessionSpecificationBuilder { + b.ephemeralMedia = &ephemeralMedia + return b +} + +// Build builds the SessionSpecification struct +func (b *SessionSpecificationBuilder) Build() (*SessionSpecification, error) { + return &SessionSpecification{ + b.clientSessionTokenTTL, + b.resourcesTTL, + b.userTrackingID, + b.notifications, + b.requestedChecks, + b.requestedTasks, + b.sdkConfig, + b.requiredDocuments, + b.blockBiometricConsent, + b.identityProfileRequirements, + b.advancedIdentityProfileRequirements, + b.createIdentityProfilePreview, + b.subject, + b.importToken, + b.ephemeralMedia, + }, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/session_spec_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/session_spec_test.go new file mode 100644 index 0000000..235b2dc --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/session_spec_test.go @@ -0,0 +1,385 @@ +package create + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/check" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/filter" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/create/task" +) + +func ExampleSessionSpecificationBuilder_Build() { + notifications, err := NewNotificationConfigBuilder(). + WithTopic("some-topic"). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + faceMatchCheck, err := check.NewRequestedFaceMatchCheckBuilder(). + WithManualCheckNever(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + documentAuthenticityCheck, err := check.NewRequestedDocumentAuthenticityCheckBuilder(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + livenessCheck, err := check.NewRequestedLivenessCheckBuilder(). + ForLivenessType("LIVENESSTYPE"). + WithMaxRetries(5). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + textExtractionTask, err := task.NewRequestedTextExtractionTaskBuilder(). + WithManualCheckFallback(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + sdkConfig, err := NewSdkConfigBuilder(). + WithAllowsCamera(). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + requiredIDDocument, err := filter.NewRequiredIDDocumentBuilder(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithClientSessionTokenTTL(789). + WithResourcesTTL(456). + WithUserTrackingID("some-tracking-id"). + WithNotifications(notifications). + WithRequestedCheck(faceMatchCheck). + WithRequestedCheck(documentAuthenticityCheck). + WithRequestedCheck(livenessCheck). + WithRequestedTask(textExtractionTask). + WithSDKConfig(sdkConfig). + WithRequiredDocument(requiredIDDocument). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"client_session_token_ttl":789,"resources_ttl":456,"user_tracking_id":"some-tracking-id","notifications":{"topics":["some-topic"]},"requested_checks":[{"type":"ID_DOCUMENT_FACE_MATCH","config":{"manual_check":"NEVER"}},{"type":"ID_DOCUMENT_AUTHENTICITY","config":{}},{"type":"LIVENESS","config":{"max_retries":5,"liveness_type":"LIVENESSTYPE"}}],"requested_tasks":[{"type":"ID_DOCUMENT_TEXT_DATA_EXTRACTION","config":{"manual_check":"FALLBACK"}}],"sdk_config":{"allowed_capture_methods":"CAMERA"},"required_documents":[{"type":"ID_DOCUMENT"}]} +} + +func ExampleSessionSpecificationBuilder_Build_withBlockBiometricConsentTrue() { + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithBlockBiometricConsent(true). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"block_biometric_consent":true} +} + +func ExampleSessionSpecificationBuilder_Build_withBlockBiometricConsentFalse() { + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithBlockBiometricConsent(false). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"block_biometric_consent":false} +} + +func ExampleSessionSpecificationBuilder_WithRequiredDocument_supplementary() { + requiredSupplementaryDocument, err := filter.NewRequiredSupplementaryDocumentBuilder(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithRequiredDocument(requiredSupplementaryDocument). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"required_documents":[{"type":"SUPPLEMENTARY_DOCUMENT"}]} +} + +func ExampleSessionSpecificationBuilder_Build_withIdentityProfileRequirements() { + identityProfile := []byte(`{ + "trust_framework": "UK_TFIDA", + "scheme": { + "type": "DBS", + "objective": "STANDARD" + } + }`) + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithIdentityProfileRequirements(identityProfile). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"identity_profile_requirements":{"trust_framework":"UK_TFIDA","scheme":{"type":"DBS","objective":"STANDARD"}}} +} + +func TestExampleSessionSpecificationBuilder_Build_WithIdentityProfileRequirements_InvalidJSON(t *testing.T) { + identityProfile := []byte(`{ + "trust_framework": UK_TFIDA", + , + }`) + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithIdentityProfileRequirements(identityProfile). + Build() + + if err != nil { + t.Errorf("error: %s", err.Error()) + return + } + + _, err = json.Marshal(sessionSpecification) + if err == nil { + t.Error("expected an error") + return + } + var marshallerErr *json.MarshalerError + if !errors.As(err, &marshallerErr) { + t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err)) + } +} + +func ExampleSessionSpecificationBuilder_Build_withAdvancedIdentityProfileRequirements() { + advancedIdentityProfile := []byte(`{ + "profiles": [ + { + "trust_framework": "UK_TFIDA", + "schemes": [ + { + "label": "LB912", + "type": "RTW" + }, + { + "label": "LB777", + "type": "DBS", + "objective": "BASIC" + } + ] + }, + { + "trust_framework": "YOTI_GLOBAL", + "schemes": [ + { + "label": "LB321", + "type": "IDENTITY", + "objective": "AL_L1", + "config": {} + } + ] + } + ] + }`) + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithAdvancedIdentityProfileRequirements(advancedIdentityProfile). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"advanced_identity_profile_requirements":{"profiles":[{"trust_framework":"UK_TFIDA","schemes":[{"label":"LB912","type":"RTW"},{"label":"LB777","type":"DBS","objective":"BASIC"}]},{"trust_framework":"YOTI_GLOBAL","schemes":[{"label":"LB321","type":"IDENTITY","objective":"AL_L1","config":{}}]}]}} +} + +func TestExampleSessionSpecificationBuilder_Build_WithAdvancedIdentityProfileRequirements_InvalidJSON(t *testing.T) { + advancedIdentityProfile := []byte(`{ + "trust_framework": UK_TFIDA", + , + }`) + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithAdvancedIdentityProfileRequirements(advancedIdentityProfile). + Build() + + if err != nil { + t.Errorf("error: %s", err.Error()) + return + } + + _, err = json.Marshal(sessionSpecification) + if err == nil { + t.Error("expected an error") + return + } + var marshallerErr *json.MarshalerError + if !errors.As(err, &marshallerErr) { + t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err)) + } +} + +func ExampleSessionSpecificationBuilder_Build_withSubject() { + subject := []byte(`{ + "subject_id": "Original subject ID" + }`) + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithSubject(subject). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"subject":{"subject_id":"Original subject ID"}} +} + +func TestExampleSessionSpecificationBuilder_Build_WithSubject_InvalidJSON(t *testing.T) { + subject := []byte(`{ + "Original subject ID" + }`) + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithSubject(subject). + Build() + + if err != nil { + t.Errorf("error: %s", err.Error()) + return + } + + _, err = json.Marshal(sessionSpecification) + if err == nil { + t.Error("expected an error") + return + } + var marshallerErr *json.MarshalerError + if !errors.As(err, &marshallerErr) { + t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err)) + } +} + +func ExampleSessionSpecificationBuilder_Build_withCreateIdentityProfilePreview() { + + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithCreateIdentityProfilePreview(true). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"create_identity_profile_preview":true} +} + +func ExampleSessionSpecificationBuilder_Build_withEphemeralMedia() { + sessionSpecification, err := NewSessionSpecificationBuilder(). + WithEphemeralMedia(true). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(sessionSpecification) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"ephemeral_media":true} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/constants.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/constants.go new file mode 100644 index 0000000..901d8ad --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/constants.go @@ -0,0 +1,6 @@ +package task + +const ( + chipDataDesired string = "DESIRED" + chipDataIgnore string = "IGNORE" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/requested_task.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/requested_task.go new file mode 100644 index 0000000..100f448 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/requested_task.go @@ -0,0 +1,12 @@ +package task + +// RequestedTask requests creation of a Task to be performed on each document +type RequestedTask interface { + Type() string + Config() RequestedTaskConfig + MarshalJSON() ([]byte, error) +} + +// RequestedTaskConfig is the configuration applied when creating a Task +type RequestedTaskConfig interface { +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/supplementary_text_extraction.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/supplementary_text_extraction.go new file mode 100644 index 0000000..7a3b626 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/supplementary_text_extraction.go @@ -0,0 +1,80 @@ +package task + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedSupplementaryDocTextExtractionTask requests creation of a Text Extraction Task +type RequestedSupplementaryDocTextExtractionTask struct { + config RequestedSupplementaryDocTextExtractionTaskConfig +} + +// Type is the type of the Requested Task +func (t *RequestedSupplementaryDocTextExtractionTask) Type() string { + return constants.SupplementaryDocumentTextDataExtraction +} + +// Config is the configuration of the Requested Task +func (t *RequestedSupplementaryDocTextExtractionTask) Config() RequestedTaskConfig { + return t.config +} + +// MarshalJSON marshals the RequestedSupplementaryDocTextExtractionTask to JSON +func (t *RequestedSupplementaryDocTextExtractionTask) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedTaskConfig `json:"config,omitempty"` + }{ + Type: t.Type(), + Config: t.Config(), + }) +} + +// NewRequestedSupplementaryDocTextExtractionTask creates a new supplementary document text extraction task +func NewRequestedSupplementaryDocTextExtractionTask(config RequestedSupplementaryDocTextExtractionTaskConfig) *RequestedSupplementaryDocTextExtractionTask { + return &RequestedSupplementaryDocTextExtractionTask{config} +} + +// RequestedSupplementaryDocTextExtractionTaskConfig is the configuration applied when creating a Text Extraction Task +type RequestedSupplementaryDocTextExtractionTaskConfig struct { + ManualCheck string `json:"manual_check,omitempty"` +} + +// NewRequestedSupplementaryDocTextExtractionTaskBuilder creates a new RequestedSupplementaryDocTextExtractionTaskBuilder +func NewRequestedSupplementaryDocTextExtractionTaskBuilder() *RequestedSupplementaryDocTextExtractionTaskBuilder { + return &RequestedSupplementaryDocTextExtractionTaskBuilder{} +} + +// RequestedSupplementaryDocTextExtractionTaskBuilder builds a RequestedSupplementaryDocTextExtractionTask +type RequestedSupplementaryDocTextExtractionTaskBuilder struct { + manualCheck string +} + +// WithManualCheckAlways sets the value of manual check to "ALWAYS" +func (builder *RequestedSupplementaryDocTextExtractionTaskBuilder) WithManualCheckAlways() *RequestedSupplementaryDocTextExtractionTaskBuilder { + builder.manualCheck = constants.Always + return builder +} + +// WithManualCheckFallback sets the value of manual check to "FALLBACK" +func (builder *RequestedSupplementaryDocTextExtractionTaskBuilder) WithManualCheckFallback() *RequestedSupplementaryDocTextExtractionTaskBuilder { + builder.manualCheck = constants.Fallback + return builder +} + +// WithManualCheckNever sets the value of manual check to "NEVER" +func (builder *RequestedSupplementaryDocTextExtractionTaskBuilder) WithManualCheckNever() *RequestedSupplementaryDocTextExtractionTaskBuilder { + builder.manualCheck = constants.Never + return builder +} + +// Build builds the RequestedSupplementaryDocTextExtractionTask +func (builder *RequestedSupplementaryDocTextExtractionTaskBuilder) Build() (*RequestedSupplementaryDocTextExtractionTask, error) { + config := RequestedSupplementaryDocTextExtractionTaskConfig{ + builder.manualCheck, + } + + return NewRequestedSupplementaryDocTextExtractionTask(config), nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/supplementary_text_extraction_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/supplementary_text_extraction_test.go new file mode 100644 index 0000000..77c12d6 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/supplementary_text_extraction_test.go @@ -0,0 +1,78 @@ +package task + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleRequestedSupplementaryDocTextExtractionTaskBuilder() { + task, err := NewRequestedSupplementaryDocTextExtractionTaskBuilder(). + WithManualCheckAlways(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION","config":{"manual_check":"ALWAYS"}} +} + +func TestRequestedSupplementaryDocTextExtractionTaskBuilder_Build_UsesLastManualCheck(t *testing.T) { + task, err := NewRequestedSupplementaryDocTextExtractionTaskBuilder(). + WithManualCheckAlways(). + WithManualCheckNever(). + WithManualCheckFallback(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedSupplementaryDocTextExtractionTaskConfig) + assert.Equal(t, "FALLBACK", config.ManualCheck) +} + +func TestRequestedSupplementaryDocTextExtractionTaskBuilder_WithManualCheckAlways(t *testing.T) { + task, err := NewRequestedSupplementaryDocTextExtractionTaskBuilder(). + WithManualCheckAlways(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedSupplementaryDocTextExtractionTaskConfig) + assert.Equal(t, "ALWAYS", config.ManualCheck) +} + +func TestRequestedSupplementaryDocTextExtractionTaskBuilder_WithManualCheckFallback(t *testing.T) { + task, err := NewRequestedSupplementaryDocTextExtractionTaskBuilder(). + WithManualCheckFallback(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedSupplementaryDocTextExtractionTaskConfig) + assert.Equal(t, "FALLBACK", config.ManualCheck) +} + +func TestRequestedSupplementaryDocTextExtractionTaskBuilder_WithManualCheckNever(t *testing.T) { + task, err := NewRequestedSupplementaryDocTextExtractionTaskBuilder(). + WithManualCheckNever(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedSupplementaryDocTextExtractionTaskConfig) + assert.Equal(t, "NEVER", config.ManualCheck) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/text_extraction.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/text_extraction.go new file mode 100644 index 0000000..6f45cd1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/text_extraction.go @@ -0,0 +1,104 @@ +package task + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// RequestedTextExtractionTask requests creation of a Text Extraction Task +type RequestedTextExtractionTask struct { + config RequestedTextExtractionTaskConfig +} + +// Type is the type of the Requested Task +func (t *RequestedTextExtractionTask) Type() string { + return constants.IDDocumentTextDataExtraction +} + +// Config is the configuration of the Requested Task +func (t *RequestedTextExtractionTask) Config() RequestedTaskConfig { + return t.config +} + +// MarshalJSON marshals the RequestedTextExtractionTask to JSON +func (t *RequestedTextExtractionTask) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Config RequestedTaskConfig `json:"config,omitempty"` + }{ + Type: t.Type(), + Config: t.Config(), + }) +} + +// NewRequestedTextExtractionTask creates a new text extraction task +func NewRequestedTextExtractionTask(config RequestedTextExtractionTaskConfig) *RequestedTextExtractionTask { + return &RequestedTextExtractionTask{config} +} + +// RequestedTextExtractionTaskConfig is the configuration applied when creating a Text Extraction Task +type RequestedTextExtractionTaskConfig struct { + ManualCheck string `json:"manual_check,omitempty"` + ChipData string `json:"chip_data,omitempty"` + CreateExpandedDocumentFields bool `json:"create_expanded_document_fields,omitempty"` +} + +// NewRequestedTextExtractionTaskBuilder creates a new RequestedTextExtractionTaskBuilder +func NewRequestedTextExtractionTaskBuilder() *RequestedTextExtractionTaskBuilder { + return &RequestedTextExtractionTaskBuilder{} +} + +// RequestedTextExtractionTaskBuilder builds a RequestedTextExtractionTask +type RequestedTextExtractionTaskBuilder struct { + manualCheck string + chipData string + createExpandedDocumentFields bool +} + +// WithManualCheckAlways sets the value of manual check to "ALWAYS" +func (builder *RequestedTextExtractionTaskBuilder) WithManualCheckAlways() *RequestedTextExtractionTaskBuilder { + builder.manualCheck = constants.Always + return builder +} + +// WithManualCheckFallback sets the value of manual check to "FALLBACK" +func (builder *RequestedTextExtractionTaskBuilder) WithManualCheckFallback() *RequestedTextExtractionTaskBuilder { + builder.manualCheck = constants.Fallback + return builder +} + +// WithManualCheckNever sets the value of manual check to "NEVER" +func (builder *RequestedTextExtractionTaskBuilder) WithManualCheckNever() *RequestedTextExtractionTaskBuilder { + builder.manualCheck = constants.Never + return builder +} + +// WithChipDataDesired sets the value of chip data to "DESIRED" +func (builder *RequestedTextExtractionTaskBuilder) WithChipDataDesired() *RequestedTextExtractionTaskBuilder { + builder.chipData = chipDataDesired + return builder +} + +// WithChipDataIgnore sets the value of chip data to "IGNORE" +func (builder *RequestedTextExtractionTaskBuilder) WithChipDataIgnore() *RequestedTextExtractionTaskBuilder { + builder.chipData = chipDataIgnore + return builder +} + +// withExpandedDocumentFields sets the value of expanded document fields whether its true or false +func (builder *RequestedTextExtractionTaskBuilder) WithExpandedDocumentFields(expandedDocumentFields bool) *RequestedTextExtractionTaskBuilder { + builder.createExpandedDocumentFields = expandedDocumentFields + return builder +} + +// Build builds the RequestedTextExtractionTask +func (builder *RequestedTextExtractionTaskBuilder) Build() (*RequestedTextExtractionTask, error) { + config := RequestedTextExtractionTaskConfig{ + builder.manualCheck, + builder.chipData, + builder.createExpandedDocumentFields, + } + + return NewRequestedTextExtractionTask(config), nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/text_extraction_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/text_extraction_test.go new file mode 100644 index 0000000..f05a74c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/create/task/text_extraction_test.go @@ -0,0 +1,115 @@ +package task + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleRequestedTextExtractionTaskBuilder() { + task, err := NewRequestedTextExtractionTaskBuilder(). + WithManualCheckAlways(). + WithChipDataIgnore(). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(task) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"ID_DOCUMENT_TEXT_DATA_EXTRACTION","config":{"manual_check":"ALWAYS","chip_data":"IGNORE"}} +} + +func TestRequestedTextExtractionTaskBuilder_Build_UsesLastManualCheck(t *testing.T) { + task, err := NewRequestedTextExtractionTaskBuilder(). + WithManualCheckAlways(). + WithManualCheckNever(). + WithManualCheckFallback(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedTextExtractionTaskConfig) + assert.Equal(t, "FALLBACK", config.ManualCheck) +} + +func TestRequestedTextExtractionTaskBuilder_WithManualCheckAlways(t *testing.T) { + task, err := NewRequestedTextExtractionTaskBuilder(). + WithManualCheckAlways(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedTextExtractionTaskConfig) + assert.Equal(t, "ALWAYS", config.ManualCheck) +} + +func TestRequestedTextExtractionTaskBuilder_WithManualCheckFallback(t *testing.T) { + task, err := NewRequestedTextExtractionTaskBuilder(). + WithManualCheckFallback(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedTextExtractionTaskConfig) + assert.Equal(t, "FALLBACK", config.ManualCheck) +} + +func TestRequestedTextExtractionTaskBuilder_WithManualCheckNever(t *testing.T) { + task, err := NewRequestedTextExtractionTaskBuilder(). + WithManualCheckNever(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedTextExtractionTaskConfig) + assert.Equal(t, "NEVER", config.ManualCheck) +} + +func TestRequestedTextExtractionTaskBuilder_WithChipDataDesired(t *testing.T) { + task, err := NewRequestedTextExtractionTaskBuilder(). + WithChipDataDesired(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedTextExtractionTaskConfig) + assert.Equal(t, "DESIRED", config.ChipData) +} + +func TestRequestedTextExtractionTaskBuilder_WithChipDataIgnore(t *testing.T) { + task, err := NewRequestedTextExtractionTaskBuilder(). + WithChipDataIgnore(). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedTextExtractionTaskConfig) + assert.Equal(t, "IGNORE", config.ChipData) +} + +func TestRequestedTextExtractionTaskBuilder_WithExpandedDocumentFields(t *testing.T) { + task, err := NewRequestedTextExtractionTaskBuilder(). + WithExpandedDocumentFields(true). + Build() + if err != nil { + t.Fail() + } + + config := task.Config().(RequestedTextExtractionTaskConfig) + assert.Equal(t, true, config.CreateExpandedDocumentFields) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/advanced_identity_profile_preview.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/advanced_identity_profile_preview.go new file mode 100644 index 0000000..4e6e36f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/advanced_identity_profile_preview.go @@ -0,0 +1,7 @@ +package retrieve + +// AdvancedIdentityProfilePreview contains info about the media needed to +// retrieve the Advanced Identity Profile Preview. +type AdvancedIdentityProfilePreview struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/advanced_identity_profile_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/advanced_identity_profile_response.go new file mode 100644 index 0000000..fe2e5de --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/advanced_identity_profile_response.go @@ -0,0 +1,10 @@ +package retrieve + +// AdvancedIdentityProfileResponse contains the SubjectId, the Result/FailureReasonResponse, verified identity details, +// and the verification reports that certifies how the identity was verified and how the verification levels were achieved. +type AdvancedIdentityProfileResponse struct { + SubjectId string `json:"subject_id"` + Result string `json:"result"` + FailureReasonResponse FailureReasonResponse `json:"failure_reason"` + Report map[string]interface{} `json:"identity_profile_report"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/breakdown_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/breakdown_response.go new file mode 100644 index 0000000..a609f6d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/breakdown_response.go @@ -0,0 +1,8 @@ +package retrieve + +// BreakdownResponse represents one breakdown item for a given check +type BreakdownResponse struct { + SubCheck string `json:"sub_check"` + Result string `json:"result"` + Details []*DetailsResponse `json:"details"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/ca_sources.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/ca_sources.go new file mode 100644 index 0000000..72bfe6b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/ca_sources.go @@ -0,0 +1,7 @@ +package retrieve + +type CASourcesResponse struct { + Type string `json:"type"` + SearchProfile string `json:"search_profile"` + Types []string `json:"types"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/capture_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/capture_response.go new file mode 100644 index 0000000..5b57703 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/capture_response.go @@ -0,0 +1,144 @@ +package retrieve + +import ( + "encoding/json" + "fmt" +) + +type CaptureResponse struct { + BiometricConsent string `json:"biometric_consent"` + RequiredResources []RequiredResourceResponse `json:"-"` +} + +type captureResponseAlias struct { + BiometricConsent string `json:"biometric_consent"` + RequiredResources []json.RawMessage `json:"required_resources"` +} + +func (c *CaptureResponse) UnmarshalJSON(data []byte) error { + aux := captureResponseAlias{} + + if err := json.Unmarshal(data, &aux); err != nil { + return fmt.Errorf("failed to unmarshal CaptureResponse: %w", err) + } + + c.BiometricConsent = aux.BiometricConsent + c.RequiredResources = make([]RequiredResourceResponse, 0, len(aux.RequiredResources)) + + for _, raw := range aux.RequiredResources { + resource, err := unmarshalResource(raw) + if err != nil { + return err + } + c.RequiredResources = append(c.RequiredResources, resource) + } + + return nil +} + +func unmarshalResource(raw json.RawMessage) (RequiredResourceResponse, error) { + var base BaseRequiredResource + if err := json.Unmarshal(raw, &base); err != nil { + return nil, fmt.Errorf("failed to unmarshal base resource: %w", err) + } + + switch base.Type { + case "ID_DOCUMENT": + var r RequiredIdDocumentResourceResponse + if err := json.Unmarshal(raw, &r); err != nil { + return nil, fmt.Errorf("failed to unmarshal ID_DOCUMENT resource: %w", err) + } + return &r, nil + + case "SUPPLEMENTARY_DOCUMENT": + var r RequiredSupplementaryDocumentResourceResponse + if err := json.Unmarshal(raw, &r); err != nil { + return nil, fmt.Errorf("failed to unmarshal SUPPLEMENTARY_DOCUMENT resource: %w", err) + } + return &r, nil + + case "LIVENESS": + switch base.LivenessType { + case "ZOOM": + var r RequiredZoomLivenessResourceResponse + if err := json.Unmarshal(raw, &r); err != nil { + return nil, fmt.Errorf("failed to unmarshal ZOOM liveness resource: %w", err) + } + return &r, nil + + case "STATIC": + var r RequiredStaticLivenessResourceResponse + if err := json.Unmarshal(raw, &r); err != nil { + return nil, fmt.Errorf("failed to unmarshal STATIC liveness resource: %w", err) + } + return &r, nil + + default: + var r RequiredLivenessResourceResponse + if err := json.Unmarshal(raw, &r); err != nil { + return nil, fmt.Errorf("failed to unmarshal generic LIVENESS resource: %w", err) + } + return &r, nil + } + + case "FACE_CAPTURE": + var r RequiredFaceCaptureResourceResponse + if err := json.Unmarshal(raw, &r); err != nil { + return nil, fmt.Errorf("failed to unmarshal FACE_CAPTURE resource: %w", err) + } + return &r, nil + + default: + var r UnknownRequiredResourceResponse + if err := json.Unmarshal(raw, &r); err != nil { + return nil, fmt.Errorf("failed to unmarshal unknown resource type: %w", err) + } + return &r, nil + } +} + +// Generic filter helper +func filterByType[T RequiredResourceResponse](resources []RequiredResourceResponse) []T { + var filtered []T + for _, r := range resources { + if typed, ok := r.(T); ok { + filtered = append(filtered, typed) + } + } + return filtered +} + +func (c *CaptureResponse) GetDocumentResourceRequirements() []RequiredResourceResponse { + var docs []RequiredResourceResponse + for _, r := range c.RequiredResources { + switch r.(type) { + case *RequiredIdDocumentResourceResponse, *RequiredSupplementaryDocumentResourceResponse: + docs = append(docs, r) + } + } + return docs +} + +func (c *CaptureResponse) GetIdDocumentResourceRequirements() []*RequiredIdDocumentResourceResponse { + return filterByType[*RequiredIdDocumentResourceResponse](c.RequiredResources) +} + +func (c *CaptureResponse) GetSupplementaryResourceRequirements() []*RequiredSupplementaryDocumentResourceResponse { + return filterByType[*RequiredSupplementaryDocumentResourceResponse](c.RequiredResources) +} + +func (c *CaptureResponse) GetZoomLivenessResourceRequirements() []*RequiredZoomLivenessResourceResponse { + return filterByType[*RequiredZoomLivenessResourceResponse](c.RequiredResources) +} + +func (c *CaptureResponse) GetStaticLivenessResourceRequirements() []*RequiredStaticLivenessResourceResponse { + return filterByType[*RequiredStaticLivenessResourceResponse](c.RequiredResources) +} + +func (c *CaptureResponse) GetLivenessResourceRequirements() []*RequiredLivenessResourceResponse { + return filterByType[*RequiredLivenessResourceResponse](c.RequiredResources) +} + +func (c *CaptureResponse) GetFaceCaptureResourceRequirements() []*RequiredFaceCaptureResourceResponse { + return filterByType[*RequiredFaceCaptureResourceResponse](c.RequiredResources) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/capture_response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/capture_response_test.go new file mode 100644 index 0000000..9e75fc8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/capture_response_test.go @@ -0,0 +1,75 @@ +package retrieve + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestCaptureResponse_UnmarshalJSON(t *testing.T) { + jsonData := []byte(`{ + "biometric_consent": "given", + "required_resources": [ + {"type": "ID_DOCUMENT", "id": "id1", "state": "pending"}, + {"type": "SUPPLEMENTARY_DOCUMENT", "id": "id2", "state": "pending"}, + {"type": "LIVENESS", "id": "id3", "state": "pending", "liveness_type": "ZOOM"}, + {"type": "LIVENESS", "id": "id4", "state": "pending", "liveness_type": "STATIC"}, + {"type": "FACE_CAPTURE", "id": "id5", "state": "pending"}, + {"type": "UNKNOWN_TYPE", "id": "id6", "state": "pending"} + ] + }`) + + var c CaptureResponse + err := json.Unmarshal(jsonData, &c) + assert.NilError(t, err) + assert.Equal(t, "given", c.BiometricConsent) + assert.Equal(t, 6, len(c.RequiredResources)) + + _, ok := c.RequiredResources[0].(*RequiredIdDocumentResourceResponse) + assert.Assert(t, ok) + + _, ok = c.RequiredResources[1].(*RequiredSupplementaryDocumentResourceResponse) + assert.Assert(t, ok) + + _, ok = c.RequiredResources[2].(*RequiredZoomLivenessResourceResponse) + assert.Assert(t, ok) + + _, ok = c.RequiredResources[3].(*RequiredStaticLivenessResourceResponse) + assert.Assert(t, ok) + + _, ok = c.RequiredResources[4].(*RequiredFaceCaptureResourceResponse) + assert.Assert(t, ok) + + _, ok = c.RequiredResources[5].(*UnknownRequiredResourceResponse) + assert.Assert(t, ok) +} + +func TestCaptureResponse_Getters(t *testing.T) { + c := CaptureResponse{ + RequiredResources: []RequiredResourceResponse{ + &RequiredIdDocumentResourceResponse{BaseRequiredResource{Type: "ID_DOCUMENT", ID: "id1"}}, + &RequiredSupplementaryDocumentResourceResponse{BaseRequiredResource{Type: "SUPPLEMENTARY_DOCUMENT", ID: "id2"}}, + &RequiredZoomLivenessResourceResponse{BaseRequiredResource{Type: "LIVENESS", ID: "id3", LivenessType: "ZOOM"}}, + &RequiredStaticLivenessResourceResponse{BaseRequiredResource{Type: "LIVENESS", ID: "id4", LivenessType: "STATIC"}}, + &RequiredFaceCaptureResourceResponse{BaseRequiredResource{Type: "FACE_CAPTURE", ID: "id5"}}, + }, + } + + assert.Equal(t, 2, len(c.GetDocumentResourceRequirements())) + assert.Equal(t, 1, len(c.GetIdDocumentResourceRequirements())) + assert.Equal(t, 1, len(c.GetSupplementaryResourceRequirements())) + assert.Equal(t, 1, len(c.GetZoomLivenessResourceRequirements())) + assert.Equal(t, 1, len(c.GetStaticLivenessResourceRequirements())) + assert.Equal(t, 1, len(c.GetFaceCaptureResourceRequirements())) +} + +func TestCaptureResponse_EmptyResources(t *testing.T) { + jsonData := []byte(`{"biometric_consent": "none", "required_resources": []}`) + + var c CaptureResponse + err := json.Unmarshal(jsonData, &c) + assert.NilError(t, err) + assert.Equal(t, "none", c.BiometricConsent) + assert.Equal(t, 0, len(c.RequiredResources)) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/check_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/check_response.go new file mode 100644 index 0000000..0643b75 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/check_response.go @@ -0,0 +1,68 @@ +package retrieve + +import ( + "time" +) + +// CheckResponse represents the attributes of a check, for any given session +type CheckResponse struct { + ID string `json:"id"` + Type string `json:"type"` + State string `json:"state"` + ResourcesUsed []string `json:"resources_used"` + GeneratedMedia []*GeneratedMedia `json:"generated_media"` + GeneratedProfile *GeneratedProfileResponse `json:"generated_profile"` + Report *ReportResponse `json:"report"` + Created *time.Time `json:"created"` + LastUpdated *time.Time `json:"last_updated"` +} + +// AuthenticityCheckResponse represents a Document Authenticity check for a given session +type AuthenticityCheckResponse struct { + *CheckResponse +} + +// FaceMatchCheckResponse represents a FaceMatch Check for a given session +type FaceMatchCheckResponse struct { + *CheckResponse +} + +// LivenessCheckResponse represents a Liveness Check for a given session +type LivenessCheckResponse struct { + *CheckResponse +} + +// TextDataCheckResponse represents a Text Data check for a given session +type TextDataCheckResponse struct { + *CheckResponse +} + +// IDDocumentComparisonCheckResponse represents an identity document comparison check for a given session +type IDDocumentComparisonCheckResponse struct { + *CheckResponse +} + +// SupplementaryDocumentTextDataCheckResponse represents a supplementary document text data check for a given session +type SupplementaryDocumentTextDataCheckResponse struct { + *CheckResponse +} + +// ThirdPartyIdentityCheckResponse represents a check with an external credit reference agency +type ThirdPartyIdentityCheckResponse struct { + *CheckResponse +} + +// WatchlistScreeningCheckResponse represents a watchlist screening check +type WatchlistScreeningCheckResponse struct { + *CheckResponse +} + +// WatchlistAdvancedCACheckResponse represents an advanced CA watchlist screening check +type WatchlistAdvancedCACheckResponse struct { + *CheckResponse +} + +// FaceComparisonCheckResponse represents an advanced CA watchlist screening check +type FaceComparisonCheckResponse struct { + *CheckResponse +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/details_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/details_response.go new file mode 100644 index 0000000..386d8c6 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/details_response.go @@ -0,0 +1,7 @@ +package retrieve + +// DetailsResponse represents a specific detail for a breakdown +type DetailsResponse struct { + Name string `json:"name"` + Value string `json:"value"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/document_fields_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/document_fields_response.go new file mode 100644 index 0000000..328a0d2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/document_fields_response.go @@ -0,0 +1,6 @@ +package retrieve + +// DocumentFieldsResponse represents the document fields in a document +type DocumentFieldsResponse struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/document_id_photo_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/document_id_photo_response.go new file mode 100644 index 0000000..5a52cc1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/document_id_photo_response.go @@ -0,0 +1,6 @@ +package retrieve + +// DocumentIDPhotoResponse represents the photo from a document +type DocumentIDPhotoResponse struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/expanded_document_fields_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/expanded_document_fields_response.go new file mode 100644 index 0000000..a55768e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/expanded_document_fields_response.go @@ -0,0 +1,6 @@ +package retrieve + +// DocumentFieldsResponse represents the document fields in a document +type ExpandedDocumentFieldsResponse struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_capture_resource_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_capture_resource_response.go new file mode 100644 index 0000000..3c04311 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_capture_resource_response.go @@ -0,0 +1,18 @@ +// face_capture_resource_response.go +package retrieve + +// FaceCaptureResourceResponse models the response for face capture resource. +type FaceCaptureResourceResponse struct { + ID string `json:"id"` + Frames int `json:"frames"` +} + +// GetID returns the ID of the face capture resource. +func (r *FaceCaptureResourceResponse) GetID() string { + return r.ID +} + +// GetFrames returns the number of image frames required. +func (r *FaceCaptureResourceResponse) GetFrames() int { + return r.Frames +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_capture_resource_response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_capture_resource_response_test.go new file mode 100644 index 0000000..e1bcf7b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_capture_resource_response_test.go @@ -0,0 +1,32 @@ +// face_capture_resource_response_test.go +package retrieve + +import ( + "encoding/json" + "gotest.tools/v3/assert" + "testing" +) + +func TestFaceCaptureResourceResponse_Getters(t *testing.T) { + resp := &FaceCaptureResourceResponse{ + ID: "face-resource-id", + Frames: 3, + } + + assert.Equal(t, resp.GetID(), "face-resource-id") + assert.Equal(t, resp.GetFrames(), 3) +} + +func TestFaceCaptureResourceResponse_UnmarshalJSON(t *testing.T) { + jsonData := `{ + "id": "resource-123", + "frames": 5 + }` + + var resp FaceCaptureResourceResponse + err := json.Unmarshal([]byte(jsonData), &resp) + assert.NilError(t, err) + + assert.Equal(t, resp.ID, "resource-123") + assert.Equal(t, resp.Frames, 5) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_map_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_map_response.go new file mode 100644 index 0000000..f3a6d64 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/face_map_response.go @@ -0,0 +1,6 @@ +package retrieve + +// FaceMapResponse represents a FaceMap response object +type FaceMapResponse struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/failure_reason_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/failure_reason_response.go new file mode 100644 index 0000000..eaf10e3 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/failure_reason_response.go @@ -0,0 +1,14 @@ +package retrieve + +type FailureReasonResponse struct { + ReasonCode string `json:"reason_code"` + RequirementsNotMetDetails []RequirementsNotMetDetail `json:"requirements_not_met_details"` +} + +type RequirementsNotMetDetail struct { + FailureType string `json:"failure_type"` + DocumentType string `json:"document_type"` + DocumentCountryIsoCode string `json:"document_country_iso_code"` + AuditId string `json:"audit_id"` + Details string `json:"details"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/file_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/file_response.go new file mode 100644 index 0000000..5893045 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/file_response.go @@ -0,0 +1,6 @@ +package retrieve + +// FileResponse represents a file +type FileResponse struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/frame_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/frame_response.go new file mode 100644 index 0000000..9055053 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/frame_response.go @@ -0,0 +1,6 @@ +package retrieve + +// FrameResponse represents a frame of a resource +type FrameResponse struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_check_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_check_response.go new file mode 100644 index 0000000..b244efa --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_check_response.go @@ -0,0 +1,17 @@ +package retrieve + +// GeneratedCheckResponse represents a check response that has been generated by the session +type GeneratedCheckResponse struct { + ID string `json:"id"` + Type string `json:"type"` +} + +// GeneratedTextDataCheckResponse represents a text data check response +type GeneratedTextDataCheckResponse struct { + *GeneratedCheckResponse +} + +// GeneratedSupplementaryTextDataCheckResponse represents a supplementary text data check response +type GeneratedSupplementaryTextDataCheckResponse struct { + *GeneratedCheckResponse +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_media.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_media.go new file mode 100644 index 0000000..540b51e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_media.go @@ -0,0 +1,7 @@ +package retrieve + +// GeneratedMedia represents media that has been generated by the session +type GeneratedMedia struct { + ID string `json:"id"` + Type string `json:"type"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_profile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_profile.go new file mode 100644 index 0000000..99ba465 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/generated_profile.go @@ -0,0 +1,6 @@ +package retrieve + +// GeneratedProfileResponse represents a profile that has been generated by the session +type GeneratedProfileResponse struct { + Media MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/get_session_result.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/get_session_result.go new file mode 100644 index 0000000..1597d5f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/get_session_result.go @@ -0,0 +1,154 @@ +package retrieve + +import ( + "encoding/json" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// GetSessionResult contains the information about a created session +type GetSessionResult struct { + ClientSessionTokenTTL int `json:"client_session_token_ttl"` + ClientSessionToken string `json:"client_session_token"` + SessionID string `json:"session_id"` + UserTrackingID string `json:"user_tracking_id"` + State string `json:"state"` + Checks []*CheckResponse `json:"checks"` + Resources *ResourceContainer `json:"resources"` + BiometricConsentTimestamp *time.Time `json:"biometric_consent"` + IdentityProfileResponse *IdentityProfileResponse `json:"identity_profile"` + AdvancedIdentityProfileResponse *AdvancedIdentityProfileResponse `json:"advanced_identity_profile"` + IdentityProfilePreview *IdentityProfilePreview `json:"identity_profile_preview"` + AdvancedIdentityProfilePreview *AdvancedIdentityProfilePreview `json:"advanced_identity_profile_preview"` + ImportTokenResponse *ImportTokenResponse `json:"import_token"` + authenticityChecks []*AuthenticityCheckResponse + faceMatchChecks []*FaceMatchCheckResponse + textDataChecks []*TextDataCheckResponse + livenessChecks []*LivenessCheckResponse + thirdPartyIdentityChecks []*ThirdPartyIdentityCheckResponse + idDocumentComparisonChecks []*IDDocumentComparisonCheckResponse + supplementaryDocumentTextDataChecks []*SupplementaryDocumentTextDataCheckResponse + watchlistScreeningChecks []*WatchlistScreeningCheckResponse + watchlistAdvancedCAChecks []*WatchlistAdvancedCACheckResponse + faceComparisonChecks []*FaceComparisonCheckResponse +} + +// AuthenticityChecks filters the checks, returning only document authenticity checks +func (g *GetSessionResult) AuthenticityChecks() []*AuthenticityCheckResponse { + return g.authenticityChecks +} + +// FaceMatchChecks filters the checks, returning only FaceMatch checks +func (g *GetSessionResult) FaceMatchChecks() []*FaceMatchCheckResponse { + return g.faceMatchChecks +} + +// TextDataChecks filters the checks, returning only ID Document Text Data checks +// Deprecated: replaced by IDDocumentTextDataChecks() +func (g *GetSessionResult) TextDataChecks() []*TextDataCheckResponse { + return g.IDDocumentTextDataChecks() +} + +// ThirdPartyIdentityChecks filters the checks, returning only external credit reference agency checks +func (g *GetSessionResult) ThirdPartyIdentityChecks() []*ThirdPartyIdentityCheckResponse { + return g.thirdPartyIdentityChecks +} + +// IDDocumentTextDataChecks filters the checks, returning only ID Document Text Data checks +func (g *GetSessionResult) IDDocumentTextDataChecks() []*TextDataCheckResponse { + return g.textDataChecks +} + +// LivenessChecks filters the checks, returning only Liveness checks +func (g *GetSessionResult) LivenessChecks() []*LivenessCheckResponse { + return g.livenessChecks +} + +// IDDocumentComparisonChecks filters the checks, returning only the identity document comparison checks +func (g *GetSessionResult) IDDocumentComparisonChecks() []*IDDocumentComparisonCheckResponse { + return g.idDocumentComparisonChecks +} + +// SupplementaryDocumentTextDataChecks filters the checks, returning only the supplementary document text data checks +func (g *GetSessionResult) SupplementaryDocumentTextDataChecks() []*SupplementaryDocumentTextDataCheckResponse { + return g.supplementaryDocumentTextDataChecks +} + +// WatchlistScreeningChecks filters the checks, returning only the Watchlist Screening checks +func (g *GetSessionResult) WatchlistScreeningChecks() []*WatchlistScreeningCheckResponse { + return g.watchlistScreeningChecks +} + +// WatchlistAdvancedCAChecks filters the checks, returning only the Watchlist Advanced CA Screening checks +func (g *GetSessionResult) WatchlistAdvancedCAChecks() []*WatchlistAdvancedCACheckResponse { + return g.watchlistAdvancedCAChecks +} + +// FaceComparisonChecks filters the checks, returning only FaceComparison checks +func (g *GetSessionResult) FaceComparisonChecks() []*FaceComparisonCheckResponse { + return g.faceComparisonChecks +} + +// UnmarshalJSON handles the custom JSON unmarshalling +func (g *GetSessionResult) UnmarshalJSON(data []byte) error { + type result GetSessionResult // declared as "type" to prevent recursive unmarshalling + if err := json.Unmarshal(data, (*result)(g)); err != nil { + return err + } + + for _, check := range g.Checks { + switch check.Type { + case constants.IDDocumentAuthenticity: + g.authenticityChecks = append(g.authenticityChecks, &AuthenticityCheckResponse{CheckResponse: check}) + + case constants.IDDocumentFaceMatch: + g.faceMatchChecks = append(g.faceMatchChecks, &FaceMatchCheckResponse{CheckResponse: check}) + + case constants.IDDocumentTextDataCheck: + g.textDataChecks = append(g.textDataChecks, &TextDataCheckResponse{CheckResponse: check}) + + case constants.Liveness: + g.livenessChecks = append(g.livenessChecks, &LivenessCheckResponse{CheckResponse: check}) + + case constants.IDDocumentComparison: + g.idDocumentComparisonChecks = append(g.idDocumentComparisonChecks, &IDDocumentComparisonCheckResponse{CheckResponse: check}) + + case constants.FaceComparison: + g.faceComparisonChecks = append(g.faceComparisonChecks, &FaceComparisonCheckResponse{CheckResponse: check}) + + case constants.ThirdPartyIdentityCheck: + g.thirdPartyIdentityChecks = append( + g.thirdPartyIdentityChecks, + &ThirdPartyIdentityCheckResponse{ + CheckResponse: check, + }) + + case constants.SupplementaryDocumentTextDataCheck: + g.supplementaryDocumentTextDataChecks = append( + g.supplementaryDocumentTextDataChecks, + &SupplementaryDocumentTextDataCheckResponse{ + CheckResponse: check, + }, + ) + + case constants.WatchlistScreening: + g.watchlistScreeningChecks = append( + g.watchlistScreeningChecks, + &WatchlistScreeningCheckResponse{ + CheckResponse: check, + }, + ) + + case constants.WatchlistAdvancedCA: + g.watchlistAdvancedCAChecks = append( + g.watchlistAdvancedCAChecks, + &WatchlistAdvancedCACheckResponse{ + CheckResponse: check, + }, + ) + } + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/get_session_result_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/get_session_result_test.go new file mode 100644 index 0000000..14457de --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/get_session_result_test.go @@ -0,0 +1,310 @@ +package retrieve_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" + "github.com/getyoti/yoti-go-sdk/v3/docscan/session/retrieve" + "github.com/getyoti/yoti-go-sdk/v3/file" + "gotest.tools/v3/assert" +) + +func TestGetSessionResult_UnmarshalJSON(t *testing.T) { + authenticityCheckResponse := &retrieve.CheckResponse{ + Type: constants.IDDocumentAuthenticity, + State: "DONE", + } + + testDate := time.Date(2020, 01, 01, 1, 2, 3, 4, time.UTC) + faceMatchCheckResponse := &retrieve.CheckResponse{ + Type: constants.IDDocumentFaceMatch, + Created: &testDate, + } + + textDataCheckResponse := &retrieve.CheckResponse{ + Type: constants.IDDocumentTextDataCheck, + Report: &retrieve.ReportResponse{}, + } + + livenessCheckResponse := &retrieve.CheckResponse{ + Type: constants.Liveness, + LastUpdated: &testDate, + } + + idDocComparisonCheckResponse := &retrieve.CheckResponse{ + Type: constants.IDDocumentComparison, + State: "PENDING", + } + + faceComparisonCheckResponse := &retrieve.CheckResponse{ + Type: constants.FaceComparison, + State: "PENDING", + } + + thirdPartyIdentityCheckResponse := &retrieve.CheckResponse{ + Type: constants.ThirdPartyIdentityCheck, + State: "PENDING", + } + + supplementaryTextDataCheckResponse := &retrieve.CheckResponse{ + Type: constants.SupplementaryDocumentTextDataCheck, + Report: &retrieve.ReportResponse{}, + } + + watchlistScreeningCheckResponse := &retrieve.CheckResponse{ + Type: constants.WatchlistScreening, + State: "DONE", + } + + advancedWatchlistScreeningCheckResponse := &retrieve.CheckResponse{ + Type: constants.WatchlistAdvancedCA, + State: "PENDING", + } + + var checks []*retrieve.CheckResponse + checks = append(checks, &retrieve.CheckResponse{Type: "OTHER_TYPE", ID: "id"}) + checks = append(checks, authenticityCheckResponse) + checks = append(checks, faceMatchCheckResponse) + checks = append(checks, textDataCheckResponse) + checks = append(checks, livenessCheckResponse) + checks = append(checks, idDocComparisonCheckResponse) + checks = append(checks, faceComparisonCheckResponse) + checks = append(checks, thirdPartyIdentityCheckResponse) + checks = append(checks, supplementaryTextDataCheckResponse) + checks = append(checks, watchlistScreeningCheckResponse) + checks = append(checks, advancedWatchlistScreeningCheckResponse) + + biometricConsentTimestamp := time.Date(2020, 01, 01, 1, 2, 3, 4, time.UTC) + + getSessionResult := retrieve.GetSessionResult{ + Checks: checks, + BiometricConsentTimestamp: &biometricConsentTimestamp, + } + marshalled, err := json.Marshal(&getSessionResult) + assert.NilError(t, err) + + var result retrieve.GetSessionResult + err = json.Unmarshal(marshalled, &result) + assert.NilError(t, err) + + assert.Equal(t, 11, len(result.Checks)) + + assert.Equal(t, 1, len(result.AuthenticityChecks())) + assert.Equal(t, "DONE", result.AuthenticityChecks()[0].State) + + assert.Equal(t, 1, len(result.FaceMatchChecks())) + assert.Check(t, result.FaceMatchChecks()[0].Created.Equal(testDate)) + + assert.Equal(t, 1, len(result.TextDataChecks())) + assert.DeepEqual(t, &retrieve.ReportResponse{}, result.TextDataChecks()[0].Report) + + assert.Equal(t, 1, len(result.IDDocumentTextDataChecks())) + assert.DeepEqual(t, &retrieve.ReportResponse{}, result.IDDocumentTextDataChecks()[0].Report) + + assert.Equal(t, 1, len(result.LivenessChecks())) + assert.Check(t, result.LivenessChecks()[0].LastUpdated.Equal(testDate)) + + assert.Equal(t, 1, len(result.IDDocumentComparisonChecks())) + assert.Equal(t, "PENDING", result.IDDocumentComparisonChecks()[0].State) + + assert.Equal(t, 1, len(result.FaceComparisonChecks())) + assert.Equal(t, "PENDING", result.FaceComparisonChecks()[0].State) + + assert.Equal(t, 1, len(result.ThirdPartyIdentityChecks())) + assert.Equal(t, "PENDING", result.ThirdPartyIdentityChecks()[0].State) + + assert.Equal(t, 1, len(result.SupplementaryDocumentTextDataChecks())) + assert.DeepEqual(t, &retrieve.ReportResponse{}, result.SupplementaryDocumentTextDataChecks()[0].Report) + assert.Assert(t, result.SupplementaryDocumentTextDataChecks()[0].Report.WatchlistSummary == nil) + assert.Assert(t, result.SupplementaryDocumentTextDataChecks()[0].GeneratedProfile == nil) + + assert.Equal(t, 1, len(result.WatchlistScreeningChecks())) + assert.DeepEqual(t, "DONE", result.WatchlistScreeningChecks()[0].State) + + assert.Equal(t, 1, len(result.WatchlistAdvancedCAChecks())) + assert.DeepEqual(t, "PENDING", result.WatchlistAdvancedCAChecks()[0].State) + + assert.Equal(t, biometricConsentTimestamp, *result.BiometricConsentTimestamp) +} + +func TestGetSessionResult_UnmarshalJSON_Watchlist(t *testing.T) { + bytes, err := file.ReadFile("../../../test/fixtures/watchlist_screening.json") + assert.NilError(t, err) + + var result retrieve.GetSessionResult + err = result.UnmarshalJSON(bytes) + assert.NilError(t, err) + + assert.Equal(t, 1, len(result.WatchlistScreeningChecks())) + watchlistScreeningCheck := result.WatchlistScreeningChecks()[0] + assert.Equal(t, watchlistScreeningCheck.GeneratedProfile.Media.Type, "JSON") + + watchlistSummary := watchlistScreeningCheck.Report.WatchlistSummary + + assert.Equal(t, 0, watchlistSummary.TotalHits) + assert.Equal(t, 2, len(watchlistSummary.SearchConfig.Categories)) + assert.Equal(t, watchlistSummary.SearchConfig.Categories[0], "ADVERSE-MEDIA") + assert.Equal(t, watchlistSummary.SearchConfig.Categories[1], "SANCTIONS") + assert.Equal(t, watchlistSummary.RawResults.Media.Type, "JSON") + assert.Equal(t, watchlistSummary.AssociatedCountryCodes[0], "GBR") +} + +func TestGetSessionResult_UnmarshalJSON_Watchlist_Advanced_CA(t *testing.T) { + bytes, err := file.ReadFile("../../../test/fixtures/watchlist_advanced_ca_profile_custom.json") + assert.NilError(t, err) + + var result retrieve.GetSessionResult + err = result.UnmarshalJSON(bytes) + assert.NilError(t, err) + + assert.Equal(t, 1, len(result.WatchlistAdvancedCAChecks())) + watchlistAdvancedCACheck := result.WatchlistAdvancedCAChecks()[0] + assert.Equal(t, 1, len(watchlistAdvancedCACheck.GeneratedMedia)) + assert.Equal(t, watchlistAdvancedCACheck.GeneratedMedia[0].Type, "JSON") + + assert.Equal(t, watchlistAdvancedCACheck.GeneratedProfile.Media.Type, "JSON") + + watchlistSummary := watchlistAdvancedCACheck.Report.WatchlistSummary + assert.Equal(t, watchlistSummary.RawResults.Media.Type, "JSON") + assert.Equal(t, watchlistSummary.AssociatedCountryCodes[0], "GBR") + assert.Equal(t, watchlistSummary.RawResults.Media.Type, "JSON") + assert.Equal(t, watchlistSummary.AssociatedCountryCodes[0], "GBR") + + searchConfig := watchlistSummary.SearchConfig + assert.Equal(t, "WITH_CUSTOM_ACCOUNT", searchConfig.Type) + assert.Equal(t, true, searchConfig.RemoveDeceased) + assert.Equal(t, true, searchConfig.ShareURL) + assert.Equal(t, "FUZZY", searchConfig.MatchingStrategy.Type) + assert.Equal(t, 0.6, searchConfig.MatchingStrategy.Fuzziness) + assert.Equal(t, "PROFILE", searchConfig.Sources.Type) + assert.Equal(t, "b41d82de-9a1d-4494-97a6-8b1b9895a908", searchConfig.Sources.SearchProfile) + assert.Equal(t, "gQ2vf0STnF5nGy9SSdyuGJuYMFfNASmV", searchConfig.APIKey) + assert.Equal(t, "111111", searchConfig.ClientRef) + assert.Equal(t, true, searchConfig.Monitoring) +} + +func TestGetSessionResult_UnmarshalJSON_Invalid(t *testing.T) { + var result retrieve.GetSessionResult + err := result.UnmarshalJSON([]byte("some-invalid-json")) + assert.ErrorContains(t, err, "invalid character") +} + +func TestGetSessionResult_UnmarshalJSON_WithoutBiometricConsentTimestamp(t *testing.T) { + var result retrieve.GetSessionResult + err := result.UnmarshalJSON([]byte("{}")) + assert.NilError(t, err) + assert.Check(t, result.BiometricConsentTimestamp == nil) +} + +func TestGetSessionResult_UnmarshalJSON_IdentityProfile(t *testing.T) { + bytes, err := file.ReadFile("../../../test/fixtures/GetSessionResultWithIdentityProfile.json") + assert.NilError(t, err) + + var result retrieve.GetSessionResult + err = result.UnmarshalJSON(bytes) + assert.NilError(t, err) + + identityProfile := result.IdentityProfileResponse + assert.Assert(t, identityProfile != nil) + + assert.Equal(t, identityProfile.SubjectId, "someStringHere") + assert.Equal(t, identityProfile.Result, "DONE") + assert.DeepEqual(t, identityProfile.FailureReasonResponse, retrieve.FailureReasonResponse{ + ReasonCode: "MANDATORY_DOCUMENT_COULD_NOT_BE_PROVIDED", + RequirementsNotMetDetails: []retrieve.RequirementsNotMetDetail{ + { + FailureType: "ID_DOCUMENT_AUTHENTICITY", + DocumentType: "PASSPORT", + DocumentCountryIsoCode: "GBR", + AuditId: "a526df5f-a9c1-4e57-8aa3-919256d8e280", + Details: "INCORRECT_DOCUMENT_TYPE", + }, + }, + }) + + assert.NilError(t, err) + tf, ok := identityProfile.Report["trust_framework"].(string) + assert.Equal(t, ok, true) + assert.Equal(t, tf, "UK_TFIDA") + media, ok := identityProfile.Report["media"].(map[string]interface{}) + assert.Equal(t, ok, true) + mid, ok := media["id"].(string) + assert.Equal(t, ok, true) + assert.Equal(t, mid, "c69ff2db-6caf-4e74-8386-037711bbc8d7") +} + +func TestGetSessionResult_UnmarshalJSON_AdvancedIdentityProfile(t *testing.T) { + bytes, err := file.ReadFile("../../../test/fixtures/GetSessionResultWithAdvancedIdentityProfile.json") + assert.NilError(t, err) + + var result retrieve.GetSessionResult + err = result.UnmarshalJSON(bytes) + assert.NilError(t, err) + + identityProfile := result.AdvancedIdentityProfileResponse + assert.Assert(t, identityProfile != nil) + + assert.Equal(t, identityProfile.SubjectId, "someStringHere") + assert.Equal(t, identityProfile.Result, "DONE") + assert.DeepEqual(t, identityProfile.FailureReasonResponse, + retrieve.FailureReasonResponse{ + ReasonCode: "MANDATORY_DOCUMENT_COULD_NOT_BE_PROVIDED", + RequirementsNotMetDetails: []retrieve.RequirementsNotMetDetail{ + { + FailureType: "ID_DOCUMENT_AUTHENTICITY", + DocumentType: "PASSPORT", + DocumentCountryIsoCode: "GBR", + AuditId: "a526df5f-a9c1-4e57-8aa3-919256d8e280", + Details: "INCORRECT_DOCUMENT_TYPE", + }}, + }, + ) + + compliances, ok := identityProfile.Report["compliance"].([]interface{}) + assert.Equal(t, ok, true) + assert.Equal(t, len(compliances), 1) + + compliance, ok := compliances[0].(map[string]interface{}) + assert.Equal(t, ok, true) + assert.Equal(t, compliance["trust_framework"], "UK_TFIDA") + + media, ok := identityProfile.Report["media"].(map[string]interface{}) + assert.Equal(t, ok, true) + mid, ok := media["id"].(string) + assert.Equal(t, ok, true) + assert.Equal(t, mid, "c69ff2db-6caf-4e74-8386-037711bbc8d7") +} + +func TestGetSessionResult_UnmarshalJSON_IdentityProfilePreview(t *testing.T) { + bytes, err := file.ReadFile("../../../test/fixtures/GetSessionResultWithIdentityProfile.json") + assert.NilError(t, err) + + var result retrieve.GetSessionResult + err = result.UnmarshalJSON(bytes) + assert.NilError(t, err) + + identityProfilePreview := result.IdentityProfilePreview + assert.Assert(t, identityProfilePreview != nil) + + assert.Assert(t, identityProfilePreview.Media != nil) + assert.Equal(t, identityProfilePreview.Media.ID, "3fa85f64-5717-4562-b3fc-2c963f66afa6") + assert.Equal(t, identityProfilePreview.Media.Type, "IMAGE") +} + +func TestGetSessionResult_UnmarshalJSON_AdvancedIdentityProfilePreview(t *testing.T) { + bytes, err := file.ReadFile("../../../test/fixtures/GetSessionResultWithAdvancedIdentityProfile.json") + assert.NilError(t, err) + + var result retrieve.GetSessionResult + err = result.UnmarshalJSON(bytes) + assert.NilError(t, err) + + identityProfilePreview := result.AdvancedIdentityProfilePreview + assert.Assert(t, identityProfilePreview != nil) + + assert.Assert(t, identityProfilePreview.Media != nil) + assert.Equal(t, identityProfilePreview.Media.ID, "3fa85f64-5717-4562-b3fc-2c963f66afa6") + assert.Equal(t, identityProfilePreview.Media.Type, "IMAGE") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_resource_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_resource_response.go new file mode 100644 index 0000000..b48bfe0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_resource_response.go @@ -0,0 +1,45 @@ +package retrieve + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// IDDocumentResourceResponse represents an Identity Document resource for a given session +type IDDocumentResourceResponse struct { + *ResourceResponse + // DocumentType is the identity document type, e.g. "PASSPORT" + DocumentType string `json:"document_type"` + // IssuingCountry is the issuing country of the identity document + IssuingCountry string `json:"issuing_country"` + // Pages are the individual pages of the identity document + Pages []*PageResponse `json:"pages"` + // DocumentFields are the associated document fields of a document + DocumentFields *DocumentFieldsResponse `json:"document_fields"` + ExpandedDocumentFields *ExpandedDocumentFieldsResponse `json:"expanded_document_fields"` + DocumentIDPhoto *DocumentIDPhotoResponse `json:"document_id_photo"` + textExtractionTasks []*TextExtractionTaskResponse +} + +// TextExtractionTasks returns a slice of text extraction tasks associated with the ID document +func (i *IDDocumentResourceResponse) TextExtractionTasks() []*TextExtractionTaskResponse { + return i.textExtractionTasks +} + +// UnmarshalJSON handles the custom JSON unmarshalling +func (i *IDDocumentResourceResponse) UnmarshalJSON(data []byte) error { + type result IDDocumentResourceResponse // declared as "type" to prevent recursive unmarshalling + if err := json.Unmarshal(data, (*result)(i)); err != nil { + return err + } + + for _, task := range i.Tasks { + switch task.Type { + case constants.IDDocumentTextDataExtraction: + i.textExtractionTasks = append(i.textExtractionTasks, &TextExtractionTaskResponse{TaskResponse: task}) + } + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_resource_response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_resource_response_test.go new file mode 100644 index 0000000..5554251 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_resource_response_test.go @@ -0,0 +1,44 @@ +package retrieve + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestIDDocumentResourceResponse_UnmarshalJSON(t *testing.T) { + idDocumentResource := &IDDocumentResourceResponse{ + ResourceResponse: &ResourceResponse{ + ID: "", + Tasks: []*TaskResponse{ + { + ID: "some-id", + Type: "ID_DOCUMENT_TEXT_DATA_EXTRACTION", + }, + { + ID: "other-id", + Type: "OTHER_TASK_TYPE", + }, + }, + }, + } + + marshalledResource, err := json.Marshal(idDocumentResource) + assert.NilError(t, err) + + var result IDDocumentResourceResponse + err = json.Unmarshal(marshalledResource, &result) + assert.NilError(t, err) + + assert.Equal(t, 2, len(result.ResourceResponse.Tasks)) + + assert.Equal(t, 1, len(result.TextExtractionTasks())) + assert.Equal(t, "some-id", result.TextExtractionTasks()[0].ID) +} + +func TestIDDocumentResourceResponse_UnmarshalJSON_Invalid(t *testing.T) { + var result IDDocumentResourceResponse + err := result.UnmarshalJSON([]byte("some-invalid-json")) + assert.ErrorContains(t, err, "invalid character") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_text_extraction_task_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_text_extraction_task_response.go new file mode 100644 index 0000000..e4124c1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_text_extraction_task_response.go @@ -0,0 +1,11 @@ +package retrieve + +// TextExtractionTaskResponse represents a Text Extraction task response +type TextExtractionTaskResponse struct { + *TaskResponse +} + +// GeneratedTextDataChecks filters the checks, returning only text data checks +func (t *TextExtractionTaskResponse) GeneratedTextDataChecks() []*GeneratedTextDataCheckResponse { + return t.generatedTextDataChecks +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_text_extraction_task_response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_text_extraction_task_response_test.go new file mode 100644 index 0000000..95066a4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/id_document_text_extraction_task_response_test.go @@ -0,0 +1,30 @@ +package retrieve + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" + "gotest.tools/v3/assert" +) + +func TestTextExtractionTaskResponse_GeneratedTextDataChecks(t *testing.T) { + var checks []*GeneratedTextDataCheckResponse + checks = append( + checks, + &GeneratedTextDataCheckResponse{ + &GeneratedCheckResponse{ + Type: constants.IDDocumentTextDataCheck, + ID: "some-id", + }, + }, + ) + + taskResponse := &TextExtractionTaskResponse{ + TaskResponse: &TaskResponse{ + generatedTextDataChecks: checks, + }, + } + + assert.Equal(t, 1, len(taskResponse.GeneratedTextDataChecks())) + assert.Equal(t, "some-id", taskResponse.GeneratedTextDataChecks()[0].ID) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/identity_profile_preview.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/identity_profile_preview.go new file mode 100644 index 0000000..258f74b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/identity_profile_preview.go @@ -0,0 +1,6 @@ +package retrieve + +// IdentityProfilePreview contains info about the media needed to retrieve the Identity Profile Preview. +type IdentityProfilePreview struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/identity_profile_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/identity_profile_response.go new file mode 100644 index 0000000..ebfe0d8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/identity_profile_response.go @@ -0,0 +1,10 @@ +package retrieve + +// IdentityProfileResponse contains the SubjectId, teh Result/FailureReasonResponse, verified identity details, +// and the verification report that certifies how the identity was verified and how the verification level was achieved. +type IdentityProfileResponse struct { + SubjectId string `json:"subject_id"` + Result string `json:"result"` + FailureReasonResponse FailureReasonResponse `json:"failure_reason"` + Report map[string]interface{} `json:"identity_profile_report"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/import_token.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/import_token.go new file mode 100644 index 0000000..a6f7bc1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/import_token.go @@ -0,0 +1,7 @@ +package retrieve + +// ImportTokenResponse contains info about the media needed to retrieve the ImportToken. +type ImportTokenResponse struct { + FailureReason string `json:"failure_reason"` + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/liveness_resource_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/liveness_resource_response.go new file mode 100644 index 0000000..91101b4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/liveness_resource_response.go @@ -0,0 +1,7 @@ +package retrieve + +// LivenessResourceResponse represents a Liveness resource for a given session +type LivenessResourceResponse struct { + *ResourceResponse + LivenessType string `json:"liveness_type"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/media_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/media_response.go new file mode 100644 index 0000000..972c51c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/media_response.go @@ -0,0 +1,11 @@ +package retrieve + +import "time" + +// MediaResponse represents a media resource +type MediaResponse struct { + ID string `json:"id"` + Type string `json:"type"` + Created *time.Time `json:"created"` + LastUpdated *time.Time `json:"last_updated"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/page_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/page_response.go new file mode 100644 index 0000000..4ea4847 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/page_response.go @@ -0,0 +1,8 @@ +package retrieve + +// PageResponse represents information about an uploaded document Page +type PageResponse struct { + CaptureMethod string `json:"capture_method"` + Media *MediaResponse `json:"media"` + Frames []*FrameResponse `json:"frames"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/recommendation_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/recommendation_response.go new file mode 100644 index 0000000..c3ac3cf --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/recommendation_response.go @@ -0,0 +1,8 @@ +package retrieve + +// RecommendationResponse represents the recommendation given for a check +type RecommendationResponse struct { + Value string `json:"value"` + Reason string `json:"reason"` + RecoverySuggestion string `json:"recovery_suggestion"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/report_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/report_response.go new file mode 100644 index 0000000..96af2e2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/report_response.go @@ -0,0 +1,8 @@ +package retrieve + +// ReportResponse represents a report for a given check +type ReportResponse struct { + Recommendation RecommendationResponse `json:"recommendation"` + Breakdown []BreakdownResponse `json:"breakdown"` + WatchlistSummary *WatchlistSummaryResponse `json:"watchlist_summary"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resource.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resource.go new file mode 100644 index 0000000..6862ea1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resource.go @@ -0,0 +1,23 @@ +package retrieve + +import "fmt" + +type RequiredResourceResponse interface { + GetType() string + String() string +} + +type BaseRequiredResource struct { + Type string `json:"type"` + ID string `json:"id"` + State string `json:"state"` + LivenessType string `json:"liveness_type,omitempty"` +} + +func (b *BaseRequiredResource) GetType() string { + return b.Type +} + +func (b *BaseRequiredResource) String() string { + return fmt.Sprintf("Type: %s, ID: %s, State: %s", b.Type, b.ID, b.State) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resource_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resource_test.go new file mode 100644 index 0000000..2b35897 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resource_test.go @@ -0,0 +1,73 @@ +package retrieve + +import ( + "gotest.tools/v3/assert" + "testing" +) + +func TestResource_Methods(t *testing.T) { + resources := []RequiredResourceResponse{ + &RequiredIdDocumentResourceResponse{ + BaseRequiredResource{ + Type: "ID_DOCUMENT", + ID: "id1", + State: "state1", + }, + }, + &RequiredSupplementaryDocumentResourceResponse{ + BaseRequiredResource{ + Type: "SUPPLEMENTARY_DOCUMENT", + ID: "id2", + State: "state2", + }, + }, + &RequiredZoomLivenessResourceResponse{ + BaseRequiredResource{ + Type: "LIVENESS", + ID: "id3", + State: "state3", + LivenessType: "ZOOM", + }, + }, + &RequiredStaticLivenessResourceResponse{ + BaseRequiredResource{ + Type: "LIVENESS", + ID: "id3", + State: "state3", + LivenessType: "STATIC", + }, + }, + &RequiredFaceCaptureResourceResponse{ + BaseRequiredResource{ + Type: "FACE_CAPTURE", + ID: "id4", + State: "state4", + }, + }, + &UnknownRequiredResourceResponse{ + BaseRequiredResource{ + Type: "UNKNOWN", + ID: "id5", + State: "state5", + }, + }, + } + + expectedTypes := []string{ + "ID_DOCUMENT", + "SUPPLEMENTARY_DOCUMENT", + "LIVENESS", + "LIVENESS", + "FACE_CAPTURE", + "UNKNOWN", + } + + for i, r := range resources { + // Test String() method + str := r.String() + assert.Assert(t, str != "", "String method should return a non-empty string for type %s", expectedTypes[i]) + + // Test GetType() method + assert.Equal(t, expectedTypes[i], r.GetType()) + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resources_types.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resources_types.go new file mode 100644 index 0000000..0ac803c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/required_resources_types.go @@ -0,0 +1,57 @@ +package retrieve + +type RequiredIdDocumentResourceResponse struct { + BaseRequiredResource +} + +func (r *RequiredIdDocumentResourceResponse) String() string { + return "ID Document Resource - " + r.BaseRequiredResource.String() +} + +type RequiredSupplementaryDocumentResourceResponse struct { + BaseRequiredResource +} + +func (r *RequiredSupplementaryDocumentResourceResponse) String() string { + return "Supplementary Document Resource - " + r.BaseRequiredResource.String() +} + +type RequiredZoomLivenessResourceResponse struct { + BaseRequiredResource +} + +func (r *RequiredZoomLivenessResourceResponse) String() string { + return "Zoom Liveness Resource - " + r.BaseRequiredResource.String() +} + +type RequiredLivenessResourceResponse struct { + BaseRequiredResource +} + +func (r *RequiredLivenessResourceResponse) String() string { + return "Liveness Resource - " + r.BaseRequiredResource.String() +} + +type RequiredStaticLivenessResourceResponse struct { + BaseRequiredResource +} + +func (r *RequiredStaticLivenessResourceResponse) String() string { + return "Static Liveness Resource - " + r.BaseRequiredResource.String() +} + +type RequiredFaceCaptureResourceResponse struct { + BaseRequiredResource +} + +func (r *RequiredFaceCaptureResourceResponse) String() string { + return "Face Capture Resource - " + r.BaseRequiredResource.String() +} + +type UnknownRequiredResourceResponse struct { + BaseRequiredResource +} + +func (r *UnknownRequiredResourceResponse) String() string { + return "Unknown Resource Type - " + r.BaseRequiredResource.String() +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_container.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_container.go new file mode 100644 index 0000000..1906949 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_container.go @@ -0,0 +1,70 @@ +package retrieve + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// ResourceContainer contains different resources that are part of the Yoti IDV session +type ResourceContainer struct { + IDDocuments []*IDDocumentResourceResponse `json:"id_documents"` + SupplementaryDocuments []*SupplementaryDocumentResourceResponse `json:"supplementary_documents"` + LivenessCapture []*LivenessResourceResponse + RawLivenessCapture []json.RawMessage `json:"liveness_capture"` + zoomLivenessResources []*ZoomLivenessResourceResponse + staticLivenessResources []*StaticLivenessResourceResponse +} + +// ZoomLivenessResources filters the liveness resources, returning only the "Zoom" liveness resources +func (r *ResourceContainer) ZoomLivenessResources() []*ZoomLivenessResourceResponse { + return r.zoomLivenessResources +} + +// ZoomLivenessResources filters the liveness resources, returning only the "Zoom" liveness resources +func (r *ResourceContainer) StaticLivenessResources() []*StaticLivenessResourceResponse { + return r.staticLivenessResources +} + +// UnmarshalJSON handles the custom JSON unmarshalling +func (r *ResourceContainer) UnmarshalJSON(data []byte) error { + type resourceContainer ResourceContainer + err := json.Unmarshal(data, (*resourceContainer)(r)) + if err != nil { + return err + } + + for _, raw := range r.RawLivenessCapture { + var v LivenessResourceResponse + err = json.Unmarshal(raw, &v) + if err != nil { + return err + } + + switch v.LivenessType { + case constants.Zoom: + var zoom ZoomLivenessResourceResponse + err = json.Unmarshal(raw, &zoom) + if err != nil { + return err + } + r.zoomLivenessResources = append(r.zoomLivenessResources, &zoom) + case constants.Static: + var static StaticLivenessResourceResponse + err = json.Unmarshal(raw, &static) + if err != nil { + return err + } + r.staticLivenessResources = append(r.staticLivenessResources, &static) + default: + err = json.Unmarshal(raw, &LivenessResourceResponse{}) + if err != nil { + return err + } + } + + r.LivenessCapture = append(r.LivenessCapture, &v) + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_container_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_container_test.go new file mode 100644 index 0000000..da68408 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_container_test.go @@ -0,0 +1,43 @@ +package retrieve + +import ( + "encoding/json" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/file" + "gotest.tools/v3/assert" +) + +func TestLivenessResourceResponse_UnmarshalJSON(t *testing.T) { + bytes, err := file.ReadFile("../../../test/fixtures/resource-container.json") + assert.NilError(t, err) + + var result ResourceContainer + err = json.Unmarshal(bytes, &result) + assert.NilError(t, err) + + assert.Equal(t, 2, len(result.LivenessCapture)) + assert.Equal(t, "ZOOM", result.LivenessCapture[0].LivenessType) + assert.Equal(t, "OTHER_LIVENESS_TYPE", result.LivenessCapture[1].LivenessType) + + assert.Equal(t, "IMAGE", result.ZoomLivenessResources()[0].Frames[0].Media.Type) + assert.Equal(t, "BINARY", result.ZoomLivenessResources()[0].FaceMap.Media.Type) +} + +func TestStaticLivenessResourceResponse_UnmarshalJSON(t *testing.T) { + bytes, err := file.ReadFile("../../../test/fixtures/resource-container-static.json") + assert.NilError(t, err) + + var result ResourceContainer + err = json.Unmarshal(bytes, &result) + assert.NilError(t, err) + + assert.Equal(t, 3, len(result.LivenessCapture)) + assert.Equal(t, "STATIC", result.LivenessCapture[0].LivenessType) +} + +func TestLivenessResourceResponse_UnmarshalJSON_Invalid(t *testing.T) { + var result ResourceContainer + err := result.UnmarshalJSON([]byte("some-invalid-json")) + assert.ErrorContains(t, err, "invalid character") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_response.go new file mode 100644 index 0000000..9e887f4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/resource_response.go @@ -0,0 +1,7 @@ +package retrieve + +// ResourceResponse represents a resource, with associated tasks +type ResourceResponse struct { + ID string `json:"id"` + Tasks []*TaskResponse `json:"tasks"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/search_config.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/search_config.go new file mode 100644 index 0000000..fd74d71 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/search_config.go @@ -0,0 +1,20 @@ +package retrieve + +type SearchConfig struct { + Type string `json:"type"` + Categories []string `json:"categories"` + RemoveDeceased bool `json:"remove_deceased"` + ShareURL bool `json:"share_url"` + Sources CASourcesResponse `json:"sources"` + MatchingStrategy CAMatchingStrategyResponse `json:"matching_strategy"` + APIKey string `json:"api_key"` + Monitoring bool `json:"monitoring"` + Tags map[string]string `json:"tags"` + ClientRef string `json:"client_ref"` +} + +type CAMatchingStrategyResponse struct { + Type string `json:"type"` + ExactMatch string `json:"exact_match"` + Fuzziness float64 `json:"fuzziness"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/session_configuration_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/session_configuration_response.go new file mode 100644 index 0000000..9dc54ac --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/session_configuration_response.go @@ -0,0 +1,52 @@ +package retrieve + +import ( + "encoding/json" + "errors" +) + +type SessionConfigurationResponse struct { + ClientSessionTokenTtl int `json:"client_session_token_ttl"` + SessionId string `json:"session_id"` + RequestedChecks []string `json:"requested_checks"` + Capture *CaptureResponse `json:"capture"` +} + +// NewSessionConfigurationResponse creates a new SessionConfigurationResponse from JSON payload bytes, +// validating mandatory fields. +func NewSessionConfigurationResponse(payload []byte) (*SessionConfigurationResponse, error) { + var resp SessionConfigurationResponse + if err := json.Unmarshal(payload, &resp); err != nil { + return nil, err + } + + if resp.ClientSessionTokenTtl <= 0 { + return nil, errors.New("client_session_token_ttl must be a positive integer") + } + if resp.SessionId == "" { + return nil, errors.New("session_id must be a non-empty string") + } + if resp.RequestedChecks == nil || len(resp.RequestedChecks) == 0 { + return nil, errors.New("requested_checks must be a non-empty array") + } + + return &resp, nil +} + +// Getter methods for each field + +func (r *SessionConfigurationResponse) GetClientSessionTokenTtl() int { + return r.ClientSessionTokenTtl +} + +func (r *SessionConfigurationResponse) GetSessionId() string { + return r.SessionId +} + +func (r *SessionConfigurationResponse) GetRequestedChecks() []string { + return r.RequestedChecks +} + +func (r *SessionConfigurationResponse) GetCapture() *CaptureResponse { + return r.Capture +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/session_configuration_response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/session_configuration_response_test.go new file mode 100644 index 0000000..8e79753 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/session_configuration_response_test.go @@ -0,0 +1,74 @@ +package retrieve + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestNewSessionConfigurationResponse_Success(t *testing.T) { + payload := SessionConfigurationResponse{ + ClientSessionTokenTtl: 3600, + SessionId: "abc123", + RequestedChecks: []string{"ID_DOCUMENT"}, + Capture: &CaptureResponse{}, // assuming zero value is acceptable + } + + data, err := json.Marshal(payload) + assert.NilError(t, err) + + result, err := NewSessionConfigurationResponse(data) + assert.NilError(t, err) + assert.Equal(t, result.ClientSessionTokenTtl, 3600) + assert.Equal(t, result.SessionId, "abc123") + assert.DeepEqual(t, result.RequestedChecks, []string{"ID_DOCUMENT"}) + assert.Assert(t, result.Capture != nil) +} + +func TestNewSessionConfigurationResponse_MissingTTL(t *testing.T) { + jsonData := `{ + "session_id": "abc123", + "requested_checks": ["ID_DOCUMENT"], + "capture": {} + }` + + _, err := NewSessionConfigurationResponse([]byte(jsonData)) + assert.ErrorContains(t, err, "client_session_token_ttl must be a positive integer") +} + +func TestNewSessionConfigurationResponse_MissingSessionID(t *testing.T) { + jsonData := `{ + "client_session_token_ttl": 3600, + "requested_checks": ["ID_DOCUMENT"], + "capture": {} + }` + + _, err := NewSessionConfigurationResponse([]byte(jsonData)) + assert.ErrorContains(t, err, "session_id must be a non-empty string") +} + +func TestNewSessionConfigurationResponse_MissingRequestedChecks(t *testing.T) { + jsonData := `{ + "client_session_token_ttl": 3600, + "session_id": "abc123", + "capture": {} + }` + + _, err := NewSessionConfigurationResponse([]byte(jsonData)) + assert.ErrorContains(t, err, "requested_checks must be a non-empty array") +} + +func TestSessionConfigurationResponse_Getters(t *testing.T) { + resp := &SessionConfigurationResponse{ + ClientSessionTokenTtl: 900, + SessionId: "test-session", + RequestedChecks: []string{"FACE_CAPTURE"}, + Capture: &CaptureResponse{}, + } + + assert.Equal(t, resp.GetClientSessionTokenTtl(), 900) + assert.Equal(t, resp.GetSessionId(), "test-session") + assert.DeepEqual(t, resp.GetRequestedChecks(), []string{"FACE_CAPTURE"}) + assert.Assert(t, resp.GetCapture() != nil) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/static_liveness_resource_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/static_liveness_resource_response.go new file mode 100644 index 0000000..d4c5061 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/static_liveness_resource_response.go @@ -0,0 +1,11 @@ +package retrieve + +// StaticLivenessResourceResponse represents a Static Liveness resource for a given session +type StaticLivenessResourceResponse struct { + *LivenessResourceResponse + Image *Image `json:"image"` +} + +type Image struct { + Media *MediaResponse `json:"media"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_resource_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_resource_response.go new file mode 100644 index 0000000..1ab6383 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_resource_response.go @@ -0,0 +1,50 @@ +package retrieve + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// SupplementaryDocumentResourceResponse represents an supplementary document resource for a given session +type SupplementaryDocumentResourceResponse struct { + *ResourceResponse + // DocumentType is the supplementary document type, e.g. "UTILITY_BILL" + DocumentType string `json:"document_type"` + // IssuingCountry is the issuing country of the supplementary document + IssuingCountry string `json:"issuing_country"` + // Pages are the individual pages of the supplementary document + Pages []*PageResponse `json:"pages"` + // DocumentFields are the associated document fields of a document + DocumentFields *DocumentFieldsResponse `json:"document_fields"` + // DocumentFile is the associated document file + DocumentFile *FileResponse `json:"file"` + textExtractionTasks []*SupplementaryDocumentTextExtractionTaskResponse +} + +// TextExtractionTasks returns a slice of text extraction tasks associated with the supplementary document +func (i *SupplementaryDocumentResourceResponse) TextExtractionTasks() []*SupplementaryDocumentTextExtractionTaskResponse { + return i.textExtractionTasks +} + +// UnmarshalJSON handles the custom JSON unmarshalling +func (i *SupplementaryDocumentResourceResponse) UnmarshalJSON(data []byte) error { + type result SupplementaryDocumentResourceResponse // declared as "type" to prevent recursive unmarshalling + if err := json.Unmarshal(data, (*result)(i)); err != nil { + return err + } + + for _, task := range i.Tasks { + switch task.Type { + case constants.SupplementaryDocumentTextDataExtraction: + i.textExtractionTasks = append( + i.textExtractionTasks, + &SupplementaryDocumentTextExtractionTaskResponse{ + TaskResponse: task, + }, + ) + } + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_resource_response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_resource_response_test.go new file mode 100644 index 0000000..c04e321 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_resource_response_test.go @@ -0,0 +1,44 @@ +package retrieve + +import ( + "encoding/json" + "testing" + + "gotest.tools/v3/assert" +) + +func TestSupplementaryDocumentResourceResponse_UnmarshalJSON(t *testing.T) { + idDocumentResource := &SupplementaryDocumentResourceResponse{ + ResourceResponse: &ResourceResponse{ + ID: "", + Tasks: []*TaskResponse{ + { + ID: "some-id", + Type: "SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION", + }, + { + ID: "other-id", + Type: "OTHER_TASK_TYPE", + }, + }, + }, + } + + marshalledResource, err := json.Marshal(idDocumentResource) + assert.NilError(t, err) + + var result SupplementaryDocumentResourceResponse + err = json.Unmarshal(marshalledResource, &result) + assert.NilError(t, err) + + assert.Equal(t, 2, len(result.ResourceResponse.Tasks)) + + assert.Equal(t, 1, len(result.TextExtractionTasks())) + assert.Equal(t, "some-id", result.TextExtractionTasks()[0].ID) +} + +func TestSupplementaryDocumentResourceResponse_UnmarshalJSON_Invalid(t *testing.T) { + var result SupplementaryDocumentResourceResponse + err := result.UnmarshalJSON([]byte("some-invalid-json")) + assert.ErrorContains(t, err, "invalid character") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_text_extraction_task_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_text_extraction_task_response.go new file mode 100644 index 0000000..c6817c0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_text_extraction_task_response.go @@ -0,0 +1,11 @@ +package retrieve + +// SupplementaryDocumentTextExtractionTaskResponse represents a Supplementary Document Text Extraction task response +type SupplementaryDocumentTextExtractionTaskResponse struct { + *TaskResponse +} + +// GeneratedTextDataChecks filters the checks, returning only text data checks +func (t *SupplementaryDocumentTextExtractionTaskResponse) GeneratedTextDataChecks() []*GeneratedSupplementaryTextDataCheckResponse { + return t.generatedSupplementaryTextDataChecks +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_text_extraction_task_response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_text_extraction_task_response_test.go new file mode 100644 index 0000000..8370a82 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/supplementary_document_text_extraction_task_response_test.go @@ -0,0 +1,28 @@ +package retrieve + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestSupplementaryDocumentTextExtractionTaskResponse_GeneratedTextDataChecks(t *testing.T) { + var checks []*GeneratedSupplementaryTextDataCheckResponse + checks = append( + checks, + &GeneratedSupplementaryTextDataCheckResponse{ + &GeneratedCheckResponse{ + ID: "some-id", + }, + }, + ) + + taskResponse := &SupplementaryDocumentTextExtractionTaskResponse{ + TaskResponse: &TaskResponse{ + generatedSupplementaryTextDataChecks: checks, + }, + } + + assert.Equal(t, 1, len(taskResponse.GeneratedTextDataChecks())) + assert.Equal(t, "some-id", taskResponse.GeneratedTextDataChecks()[0].ID) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/task_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/task_response.go new file mode 100644 index 0000000..ca4e779 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/task_response.go @@ -0,0 +1,56 @@ +package retrieve + +import ( + "encoding/json" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" +) + +// TaskResponse represents the attributes of a task, for any given session +type TaskResponse struct { + ID string `json:"id"` + Type string `json:"type"` + State string `json:"state"` + Created *time.Time `json:"created"` + LastUpdated *time.Time `json:"last_updated"` + GeneratedChecks []*GeneratedCheckResponse `json:"generated_checks"` + GeneratedMedia []*GeneratedMedia `json:"generated_media"` + generatedTextDataChecks []*GeneratedTextDataCheckResponse + generatedSupplementaryTextDataChecks []*GeneratedSupplementaryTextDataCheckResponse +} + +// GeneratedTextDataChecks filters the checks, returning only text data checks +// Deprecated: this function is now implemented on specific task types +func (t *TaskResponse) GeneratedTextDataChecks() []*GeneratedTextDataCheckResponse { + return t.generatedTextDataChecks +} + +// UnmarshalJSON handles the custom JSON unmarshalling +func (t *TaskResponse) UnmarshalJSON(data []byte) error { + type result TaskResponse // declared as "type" to prevent recursive unmarshalling + if err := json.Unmarshal(data, (*result)(t)); err != nil { + return err + } + + for _, check := range t.GeneratedChecks { + switch check.Type { + case constants.IDDocumentTextDataCheck: + t.generatedTextDataChecks = append( + t.generatedTextDataChecks, + &GeneratedTextDataCheckResponse{ + GeneratedCheckResponse: check, + }, + ) + case constants.SupplementaryDocumentTextDataCheck: + t.generatedSupplementaryTextDataChecks = append( + t.generatedSupplementaryTextDataChecks, + &GeneratedSupplementaryTextDataCheckResponse{ + GeneratedCheckResponse: check, + }, + ) + } + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/task_response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/task_response_test.go new file mode 100644 index 0000000..fb5f6cf --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/task_response_test.go @@ -0,0 +1,51 @@ +package retrieve + +import ( + "encoding/json" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/docscan/constants" + "gotest.tools/v3/assert" +) + +func TestTaskResponse_UnmarshalJSON(t *testing.T) { + checks := []*GeneratedCheckResponse{ + { + Type: constants.IDDocumentTextDataCheck, + ID: "some-id", + }, + { + Type: constants.SupplementaryDocumentTextDataCheck, + ID: "supplementary-id", + }, + { + Type: "OTHER_TYPE", + ID: "other-id", + }, + } + + taskResponse := TaskResponse{ + GeneratedChecks: checks, + } + marshalled, err := json.Marshal(&taskResponse) + assert.NilError(t, err) + + var result TaskResponse + err = json.Unmarshal(marshalled, &result) + assert.NilError(t, err) + + assert.Equal(t, 1, len(result.GeneratedTextDataChecks())) + assert.Equal(t, "some-id", result.GeneratedTextDataChecks()[0].ID) + + assert.Equal(t, 1, len(result.generatedTextDataChecks)) + assert.Equal(t, "some-id", result.generatedTextDataChecks[0].ID) + + assert.Equal(t, 1, len(result.generatedSupplementaryTextDataChecks)) + assert.Equal(t, "supplementary-id", result.generatedSupplementaryTextDataChecks[0].ID) +} + +func TestTaskResponse_UnmarshalJSON_Invalid(t *testing.T) { + var result TaskResponse + err := result.UnmarshalJSON([]byte("some-invalid-json")) + assert.ErrorContains(t, err, "invalid character") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/watchlist.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/watchlist.go new file mode 100644 index 0000000..4ce123f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/watchlist.go @@ -0,0 +1,12 @@ +package retrieve + +type RawResults struct { + Media MediaResponse `json:"media"` +} + +type WatchlistSummaryResponse struct { + TotalHits int `json:"total_hits"` + RawResults RawResults `json:"raw_results"` + AssociatedCountryCodes []string `json:"associated_country_codes"` + SearchConfig SearchConfig `json:"search_config"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/zoom_liveness_resource_response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/zoom_liveness_resource_response.go new file mode 100644 index 0000000..e9276eb --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/session/retrieve/zoom_liveness_resource_response.go @@ -0,0 +1,8 @@ +package retrieve + +// ZoomLivenessResourceResponse represents a Zoom Liveness resource for a given session +type ZoomLivenessResourceResponse struct { + *LivenessResourceResponse + FaceMap *FaceMapResponse `json:"facemap"` + Frames []*FrameResponse `json:"frames"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/supported/supported_documents.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/supported/supported_documents.go new file mode 100644 index 0000000..bce18e4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/docscan/supported/supported_documents.go @@ -0,0 +1,18 @@ +package supported + +// DocumentsResponse details the supported countries and associated documents +type DocumentsResponse struct { + SupportedCountries []*Country `json:"supported_countries"` +} + +// Country details the supported documents for a particular country +type Country struct { + Code string `json:"code"` + SupportedDocuments []*Document `json:"supported_documents"` +} + +// Document is the document type that is supported +type Document struct { + Type string `json:"type"` + IsStrictlyLatin bool `json:"is_strictly_latin"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/policy_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/policy_builder.go new file mode 100644 index 0000000..e7f8930 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/policy_builder.go @@ -0,0 +1,261 @@ +package dynamic + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" +) + +const ( + authTypeSelfieConst = 1 + authTypePinConst = 2 +) + +// PolicyBuilder constructs a json payload specifying the dynamic policy +// for a dynamic scenario +type PolicyBuilder struct { + wantedAttributes map[string]WantedAttribute + wantedAuthTypes map[int]bool + isWantedRememberMe bool + err error + identityProfileRequirements *json.RawMessage + advancedIdentityProfileRequirements *json.RawMessage +} + +// Policy represents a dynamic policy for a share +type Policy struct { + attributes []WantedAttribute + authTypes []int + rememberMeID bool + identityProfileRequirements *json.RawMessage + advancedIdentityProfileRequirements *json.RawMessage +} + +// WithWantedAttribute adds an attribute from WantedAttributeBuilder to the policy +func (b *PolicyBuilder) WithWantedAttribute(attribute WantedAttribute) *PolicyBuilder { + if b.wantedAttributes == nil { + b.wantedAttributes = make(map[string]WantedAttribute) + } + var key string + if attribute.derivation != "" { + key = attribute.derivation + } else { + key = attribute.name + } + b.wantedAttributes[key] = attribute + return b +} + +// WithWantedAttributeByName adds an attribute by its name. This is not the preferred +// way of adding an attribute - instead use the other methods below. +// Options allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithWantedAttributeByName(name string, options ...interface{}) *PolicyBuilder { + attributeBuilder := (&WantedAttributeBuilder{}).WithName(name) + + for _, option := range options { + switch value := option.(type) { + case SourceConstraint: + attributeBuilder.WithConstraint(&value) + case constraintInterface: + attributeBuilder.WithConstraint(value) + default: + panic(fmt.Sprintf("not a valid option type, %v", value)) + } + } + + attribute, err := attributeBuilder.Build() + if err != nil { + b.err = yotierror.MultiError{This: err, Next: b.err} + } + b.WithWantedAttribute(attribute) + return b +} + +// WithFamilyName adds the family name attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithFamilyName(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrFamilyName, options...) +} + +// WithGivenNames adds the given names attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithGivenNames(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrGivenNames, options...) +} + +// WithFullName adds the full name attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithFullName(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrFullName, options...) +} + +// WithDateOfBirth adds the date of birth attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithDateOfBirth(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrDateOfBirth, options...) +} + +// WithGender adds the gender attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithGender(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrGender, options...) +} + +// WithPostalAddress adds the postal address attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithPostalAddress(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrAddress, options...) +} + +// WithStructuredPostalAddress adds the structured postal address attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithStructuredPostalAddress(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrStructuredPostalAddress, options...) +} + +// WithNationality adds the nationality attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithNationality(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrNationality, options...) +} + +// WithPhoneNumber adds the phone number attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithPhoneNumber(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrMobileNumber, options...) +} + +// WithSelfie adds the selfie attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithSelfie(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrSelfie, options...) +} + +// WithEmail adds the email address attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithEmail(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrEmailAddress, options...) +} + +// WithDocumentImages adds the document images attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithDocumentImages(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrDocumentImages, options...) +} + +// WithDocumentDetails adds the document details attribute, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithDocumentDetails(options ...interface{}) *PolicyBuilder { + return b.WithWantedAttributeByName(consts.AttrDocumentDetails, options...) +} + +// WithAgeDerivedAttribute is a helper method for setting age based derivations +// Prefer to use WithAgeOver and WithAgeUnder instead of using this directly. +// "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithAgeDerivedAttribute(derivation string, options ...interface{}) *PolicyBuilder { + var attributeBuilder WantedAttributeBuilder + attributeBuilder. + WithName(consts.AttrDateOfBirth). + WithDerivation(derivation) + + for _, option := range options { + switch value := option.(type) { + case SourceConstraint: + attributeBuilder.WithConstraint(&value) + case constraintInterface: + attributeBuilder.WithConstraint(value) + default: + panic(fmt.Sprintf("not a valid option type, %v", value)) + } + } + + attr, err := attributeBuilder.Build() + if err != nil { + b.err = yotierror.MultiError{This: err, Next: b.err} + } + return b.WithWantedAttribute(attr) +} + +// WithAgeOver sets this dynamic policy as requesting whether the user is older than a certain age. +// "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithAgeOver(age int, options ...interface{}) *PolicyBuilder { + return b.WithAgeDerivedAttribute(fmt.Sprintf(consts.AttrAgeOver, age), options...) +} + +// WithAgeUnder sets this dynamic policy as requesting whether the user is younger +// than a certain age, "options" allows one or more options to be specified e.g. SourceConstraint +func (b *PolicyBuilder) WithAgeUnder(age int, options ...interface{}) *PolicyBuilder { + return b.WithAgeDerivedAttribute(fmt.Sprintf(consts.AttrAgeUnder, age), options...) +} + +// WithWantedRememberMe sets the Policy as requiring a "Remember Me ID" +func (b *PolicyBuilder) WithWantedRememberMe() *PolicyBuilder { + b.isWantedRememberMe = true + return b +} + +// WithWantedAuthType sets this dynamic policy as requiring a specific authentication type +func (b *PolicyBuilder) WithWantedAuthType(wantedAuthType int) *PolicyBuilder { + if b.wantedAuthTypes == nil { + b.wantedAuthTypes = make(map[int]bool) + } + b.wantedAuthTypes[wantedAuthType] = true + return b +} + +// WithSelfieAuth sets this dynamic policy as requiring Selfie-based authentication +func (b *PolicyBuilder) WithSelfieAuth() *PolicyBuilder { + return b.WithWantedAuthType(authTypeSelfieConst) +} + +// WithPinAuth sets this dynamic policy as requiring PIN authentication +func (b *PolicyBuilder) WithPinAuth() *PolicyBuilder { + return b.WithWantedAuthType(authTypePinConst) +} + +// WithIdentityProfileRequirements adds Identity Profile Requirements to the policy. Must be valid JSON. +func (b *PolicyBuilder) WithIdentityProfileRequirements(identityProfile json.RawMessage) *PolicyBuilder { + b.identityProfileRequirements = &identityProfile + return b +} + +// WithAdvancedIdentityProfileRequirements adds Advanced Identity Profile Requirements to the policy. Must be valid JSON. +func (b *PolicyBuilder) WithAdvancedIdentityProfileRequirements(advancedIdentityProfile json.RawMessage) *PolicyBuilder { + b.advancedIdentityProfileRequirements = &advancedIdentityProfile + return b +} + +// Build constructs a dynamic policy object +func (b *PolicyBuilder) Build() (Policy, error) { + return Policy{ + attributes: b.attributesAsList(), + authTypes: b.authTypesAsList(), + rememberMeID: b.isWantedRememberMe, + identityProfileRequirements: b.identityProfileRequirements, + advancedIdentityProfileRequirements: b.advancedIdentityProfileRequirements, + }, b.err +} + +func (b *PolicyBuilder) attributesAsList() []WantedAttribute { + attributeList := make([]WantedAttribute, 0) + for _, attr := range b.wantedAttributes { + attributeList = append(attributeList, attr) + } + return attributeList +} + +func (b *PolicyBuilder) authTypesAsList() []int { + authTypeList := make([]int, 0) + for auth, boolValue := range b.wantedAuthTypes { + if boolValue { + authTypeList = append(authTypeList, auth) + } + } + return authTypeList +} + +// MarshalJSON returns the JSON encoding +func (policy *Policy) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Wanted []WantedAttribute `json:"wanted"` + WantedAuthTypes []int `json:"wanted_auth_types"` + WantedRememberMe bool `json:"wanted_remember_me"` + IdentityProfileRequirements *json.RawMessage `json:"identity_profile_requirements,omitempty"` + AdvancedIdentityProfileRequirements *json.RawMessage `json:"advanced_identity_profile_requirements,omitempty"` + }{ + Wanted: policy.attributes, + WantedAuthTypes: policy.authTypes, + WantedRememberMe: policy.rememberMeID, + IdentityProfileRequirements: policy.identityProfileRequirements, + AdvancedIdentityProfileRequirements: policy.advancedIdentityProfileRequirements, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/policy_builder_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/policy_builder_test.go new file mode 100644 index 0000000..7402e67 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/policy_builder_test.go @@ -0,0 +1,533 @@ +package dynamic + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" + "gotest.tools/v3/assert" +) + +func ExamplePolicyBuilder_WithFamilyName() { + policy, err := (&PolicyBuilder{}).WithFamilyName().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"family_name","accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithDocumentDetails() { + policy, err := (&PolicyBuilder{}).WithDocumentDetails().Build() + if err != nil { + return + } + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"document_details","accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithDocumentImages() { + policy, err := (&PolicyBuilder{}).WithDocumentImages().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"document_images","accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithSelfie() { + policy, err := (&PolicyBuilder{}).WithSelfie().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"selfie","accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithAgeOver() { + constraint, err := (&SourceConstraintBuilder{}).WithDrivingLicence("").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + policy, err := (&PolicyBuilder{}).WithAgeOver(18, constraint).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.attributes[0].MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"name":"date_of_birth","derivation":"age_over:18","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[{"name":"DRIVING_LICENCE","sub_type":""}],"soft_preference":false}}],"accept_self_asserted":false} +} + +func ExamplePolicyBuilder_WithSelfieAuth() { + policy, err := (&PolicyBuilder{}).WithSelfieAuth().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[],"wanted_auth_types":[1],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithWantedRememberMe() { + policy, err := (&PolicyBuilder{}).WithWantedRememberMe().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[],"wanted_auth_types":[],"wanted_remember_me":true} +} + +func ExamplePolicyBuilder_WithFullName() { + constraint, err := (&SourceConstraintBuilder{}).WithPassport("").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + policy, err := (&PolicyBuilder{}).WithFullName(&constraint).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + marshalledJSON, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"wanted":[{"name":"full_name","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[{"name":"PASSPORT","sub_type":""}],"soft_preference":false}}],"accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder() { + policy, err := (&PolicyBuilder{}).WithFullName(). + WithPinAuth().WithWantedRememberMe().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"full_name","accept_self_asserted":false}],"wanted_auth_types":[2],"wanted_remember_me":true} +} + +func ExamplePolicyBuilder_WithAgeUnder() { + policy, err := (&PolicyBuilder{}).WithAgeUnder(18).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"date_of_birth","derivation":"age_under:18","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithGivenNames() { + policy, err := (&PolicyBuilder{}).WithGivenNames().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"given_names","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithDateOfBirth() { + policy, err := (&PolicyBuilder{}).WithDateOfBirth().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"date_of_birth","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithGender() { + policy, err := (&PolicyBuilder{}).WithGender().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"gender","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithPostalAddress() { + policy, err := (&PolicyBuilder{}).WithPostalAddress().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"postal_address","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithStructuredPostalAddress() { + policy, err := (&PolicyBuilder{}).WithStructuredPostalAddress().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"structured_postal_address","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithNationality() { + policy, err := (&PolicyBuilder{}).WithNationality().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"nationality","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func ExamplePolicyBuilder_WithPhoneNumber() { + policy, err := (&PolicyBuilder{}).WithPhoneNumber().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[{"name":"phone_number","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false} +} + +func TestDynamicPolicyBuilder_WithWantedAttributeByName_WithSourceConstraint(t *testing.T) { + attributeName := "attributeName" + builder := &PolicyBuilder{} + sourceConstraint, err := (&SourceConstraintBuilder{}).Build() + assert.NilError(t, err) + + builder.WithWantedAttributeByName( + attributeName, + sourceConstraint, + ) + + policy, err := builder.Build() + assert.NilError(t, err) + assert.Equal(t, len(policy.attributes), 1) + assert.Equal(t, policy.attributes[0].name, attributeName) + assert.Equal(t, len(policy.attributes[0].constraints), 1) +} + +func TestDynamicPolicyBuilder_WithWantedAttributeByName_InvalidOptionsShouldPanic(t *testing.T) { + attributeName := "attributeName" + builder := &PolicyBuilder{} + invalidOption := "invalidOption" + + defer func() { + r := recover().(string) + assert.Check(t, strings.Contains(r, "not a valid option type")) + }() + + builder.WithWantedAttributeByName( + attributeName, + invalidOption, + ) + + t.Error("Expected Panic") + +} + +func TestDynamicPolicyBuilder_WithWantedAttributeByName_ShouldPropagateErrors(t *testing.T) { + builder := &PolicyBuilder{} + + builder.WithWantedAttributeByName("") + builder.WithWantedAttributeByName("") + + _, err := builder.Build() + + assert.Error(t, err, "Wanted attribute names must not be empty, Wanted attribute names must not be empty") + assert.Error(t, err.(yotierror.MultiError).Unwrap(), "Wanted attribute names must not be empty") +} + +func TestDynamicPolicyBuilder_WithAgeDerivedAttribute_WithSourceConstraint(t *testing.T) { + builder := &PolicyBuilder{} + sourceConstraint, err := (&SourceConstraintBuilder{}).Build() + assert.NilError(t, err) + + builder.WithAgeDerivedAttribute( + fmt.Sprintf(consts.AttrAgeOver, 18), + sourceConstraint, + ) + + policy, err := builder.Build() + assert.NilError(t, err) + assert.Equal(t, len(policy.attributes), 1) + assert.Equal(t, policy.attributes[0].name, consts.AttrDateOfBirth) + assert.Equal(t, len(policy.attributes[0].constraints), 1) +} + +func TestDynamicPolicyBuilder_WithAgeDerivedAttribute_WithConstraintInterface(t *testing.T) { + builder := &PolicyBuilder{} + var constraint constraintInterface + sourceConstraint, err := (&SourceConstraintBuilder{}).Build() + constraint = &sourceConstraint + assert.NilError(t, err) + + builder.WithAgeDerivedAttribute( + fmt.Sprintf(consts.AttrAgeOver, 18), + constraint, + ) + + policy, err := builder.Build() + assert.NilError(t, err) + assert.Equal(t, len(policy.attributes), 1) + assert.Equal(t, policy.attributes[0].name, consts.AttrDateOfBirth) + assert.Equal(t, len(policy.attributes[0].constraints), 1) +} + +func TestDynamicPolicyBuilder_WithAgeDerivedAttribute_InvalidOptionsShouldPanic(t *testing.T) { + builder := &PolicyBuilder{} + invalidOption := "invalidOption" + + defer func() { + r := recover().(string) + assert.Check(t, strings.Contains(r, "not a valid option type")) + }() + + builder.WithAgeDerivedAttribute( + fmt.Sprintf(consts.AttrAgeOver, 18), + invalidOption, + ) + + t.Error("Expected Panic") + +} + +func ExamplePolicyBuilder_WithIdentityProfileRequirements() { + identityProfile := []byte(`{ + "trust_framework": "UK_TFIDA", + "scheme": { + "type": "DBS", + "objective": "STANDARD" + } + }`) + + policy, err := (&PolicyBuilder{}).WithIdentityProfileRequirements(identityProfile).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[],"wanted_auth_types":[],"wanted_remember_me":false,"identity_profile_requirements":{"trust_framework":"UK_TFIDA","scheme":{"type":"DBS","objective":"STANDARD"}}} +} + +func TestPolicyBuilder_WithIdentityProfileRequirements_ShouldFailForInvalidJSON(t *testing.T) { + identityProfile := []byte(`{ + "trust_framework": UK_TFIDA", + , + }`) + + policy, err := (&PolicyBuilder{}).WithIdentityProfileRequirements(identityProfile).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + _, err = policy.MarshalJSON() + if err == nil { + t.Error("expected an error") + } + var marshallerErr *json.MarshalerError + if !errors.As(err, &marshallerErr) { + t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err)) + } +} + +func ExamplePolicyBuilder_WithAdvancedIdentityProfileRequirements() { + advancedIdentityProfile := []byte(`{ + "profiles": [ + { + "trust_framework": "UK_TFIDA", + "schemes": [ + { + "label": "LB912", + "type": "RTW" + }, + { + "label": "LB777", + "type": "DBS", + "objective": "BASIC" + } + ] + }, + { + "trust_framework": "YOTI_GLOBAL", + "schemes": [ + { + "label": "LB321", + "type": "IDENTITY", + "objective": "AL_L1", + "config": {} + } + ] + } + ] + }`) + + policy, err := (&PolicyBuilder{}).WithAdvancedIdentityProfileRequirements(advancedIdentityProfile).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := policy.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"wanted":[],"wanted_auth_types":[],"wanted_remember_me":false,"advanced_identity_profile_requirements":{"profiles":[{"trust_framework":"UK_TFIDA","schemes":[{"label":"LB912","type":"RTW"},{"label":"LB777","type":"DBS","objective":"BASIC"}]},{"trust_framework":"YOTI_GLOBAL","schemes":[{"label":"LB321","type":"IDENTITY","objective":"AL_L1","config":{}}]}]}} +} + +func TestPolicyBuilder_WithAdvancedIdentityProfileRequirements_ShouldFailForInvalidJSON(t *testing.T) { + advancedIdentityProfile := []byte(`{ + "trust_framework": UK_TFIDA", + , + }`) + + policy, err := (&PolicyBuilder{}).WithAdvancedIdentityProfileRequirements(advancedIdentityProfile).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + _, err = policy.MarshalJSON() + if err == nil { + t.Error("expected an error") + } + var marshallerErr *json.MarshalerError + if !errors.As(err, &marshallerErr) { + t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err)) + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/scenario_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/scenario_builder.go new file mode 100644 index 0000000..fdb4df4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/scenario_builder.go @@ -0,0 +1,73 @@ +package dynamic + +import ( + "encoding/json" +) + +// ScenarioBuilder builds a dynamic scenario +type ScenarioBuilder struct { + scenario Scenario + err error +} + +// Scenario represents a dynamic scenario +type Scenario struct { + policy *Policy + extensions []interface{} + callbackEndpoint string + subject *json.RawMessage +} + +// WithPolicy attaches a DynamicPolicy to the DynamicScenario +func (builder *ScenarioBuilder) WithPolicy(policy Policy) *ScenarioBuilder { + builder.scenario.policy = &policy + return builder +} + +// WithExtension adds an extension to the scenario +func (builder *ScenarioBuilder) WithExtension(extension interface{}) *ScenarioBuilder { + builder.scenario.extensions = append(builder.scenario.extensions, extension) + return builder +} + +// WithCallbackEndpoint sets the callback URL +func (builder *ScenarioBuilder) WithCallbackEndpoint(endpoint string) *ScenarioBuilder { + builder.scenario.callbackEndpoint = endpoint + return builder +} + +// WithSubject adds a subject to the scenario. Must be valid JSON. +func (builder *ScenarioBuilder) WithSubject(subject json.RawMessage) *ScenarioBuilder { + builder.scenario.subject = &subject + return builder +} + +// Build constructs the DynamicScenario +func (builder *ScenarioBuilder) Build() (Scenario, error) { + if builder.scenario.extensions == nil { + builder.scenario.extensions = make([]interface{}, 0) + } + if builder.scenario.policy == nil { + policy, err := (&PolicyBuilder{}).Build() + if err != nil { + return builder.scenario, err + } + builder.scenario.policy = &policy + } + return builder.scenario, builder.err +} + +// MarshalJSON returns the JSON encoding +func (scenario Scenario) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Policy Policy `json:"policy"` + Extensions []interface{} `json:"extensions"` + CallbackEndpoint string `json:"callback_endpoint"` + Subject *json.RawMessage `json:"subject,omitempty"` + }{ + Policy: *scenario.policy, + Extensions: scenario.extensions, + CallbackEndpoint: scenario.callbackEndpoint, + Subject: scenario.subject, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/scenario_builder_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/scenario_builder_test.go new file mode 100644 index 0000000..cbef8cb --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/scenario_builder_test.go @@ -0,0 +1,124 @@ +package dynamic + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/extension" +) + +func ExampleScenarioBuilder() { + scenario, err := (&ScenarioBuilder{}).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := scenario.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"policy":{"wanted":[],"wanted_auth_types":[],"wanted_remember_me":false},"extensions":[],"callback_endpoint":""} +} + +func ExampleScenarioBuilder_WithPolicy() { + policy, err := (&PolicyBuilder{}).WithEmail().WithPinAuth().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + scenario, err := (&ScenarioBuilder{}).WithPolicy(policy).WithCallbackEndpoint("/foo").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := scenario.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"policy":{"wanted":[{"name":"email_address","accept_self_asserted":false}],"wanted_auth_types":[2],"wanted_remember_me":false},"extensions":[],"callback_endpoint":"/foo"} +} + +func ExampleScenarioBuilder_WithExtension() { + policy, err := (&PolicyBuilder{}).WithFullName().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + builtExtension, err := (&extension.TransactionalFlowExtensionBuilder{}). + WithContent("Transactional Flow Extension"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + scenario, err := (&ScenarioBuilder{}).WithExtension(builtExtension).WithPolicy(policy).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := scenario.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"policy":{"wanted":[{"name":"full_name","accept_self_asserted":false}],"wanted_auth_types":[],"wanted_remember_me":false},"extensions":[{"type":"TRANSACTIONAL_FLOW","content":"Transactional Flow Extension"}],"callback_endpoint":""} +} + +func ExampleScenarioBuilder_WithSubject() { + subject := []byte(`{ + "subject_id": "some_subject_id_string" + }`) + + scenario, err := (&ScenarioBuilder{}).WithSubject(subject).WithCallbackEndpoint("/foo").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := scenario.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"policy":{"wanted":[],"wanted_auth_types":[],"wanted_remember_me":false},"extensions":[],"callback_endpoint":"/foo","subject":{"subject_id":"some_subject_id_string"}} +} + +func TestScenarioBuilder_WithSubject_ShouldFailForInvalidJSON(t *testing.T) { + subject := []byte(`{ + subject_id: some_subject_id_string + }`) + + scenario, err := (&ScenarioBuilder{}).WithSubject(subject).WithCallbackEndpoint("/foo").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + _, err = scenario.MarshalJSON() + if err == nil { + t.Error("expected an error") + } + var marshallerErr *json.MarshalerError + if !errors.As(err, &marshallerErr) { + t.Errorf("wanted err to be of type '%v', got: '%v'", reflect.TypeOf(marshallerErr), reflect.TypeOf(err)) + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/service.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/service.go new file mode 100644 index 0000000..ed44cdb --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/service.go @@ -0,0 +1,55 @@ +package dynamic + +import ( + "crypto/rsa" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/getyoti/yoti-go-sdk/v3/requests" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" +) + +func getDynamicShareEndpoint(clientSdkId string) string { + return fmt.Sprintf( + "/qrcodes/apps/%s", + clientSdkId, + ) +} + +// CreateShareURL creates a QR code for a dynamic scenario +func CreateShareURL(httpClient requests.HttpClient, scenario *Scenario, clientSdkId, apiUrl string, key *rsa.PrivateKey) (share ShareURL, err error) { + endpoint := getDynamicShareEndpoint(clientSdkId) + + payload, err := scenario.MarshalJSON() + if err != nil { + return + } + + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodPost, + BaseURL: apiUrl, + Endpoint: endpoint, + Headers: nil, + Body: payload, + }.Request() + if err != nil { + return + } + + response, err := requests.Execute(httpClient, request, ShareURLHTTPErrorMessages, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return share, err + } + + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return + } + + err = json.Unmarshal(responseBytes, &share) + + return +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/service_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/service_test.go new file mode 100644 index 0000000..9057bc1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/service_test.go @@ -0,0 +1,115 @@ +package dynamic + +import ( + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "gotest.tools/v3/assert" +) + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func ExampleCreateShareURL() { + key := test.GetValidKey("../test/test-key.pem") + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{"qrcode":"https://code.yoti.com/CAEaJDQzNzllZDc0LTU0YjItNDkxMy04OTE4LTExYzM2ZDU2OTU3ZDAC","ref_id":"0"}`)), + }, nil + }, + } + + policy, err := (&PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + scenario, err := (&ScenarioBuilder{}).WithPolicy(policy).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + result, err := CreateShareURL(client, &scenario, "sdkId", "https://apiurl", key) + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Printf("QR code: %s", result.ShareURL) + // Output: QR code: https://code.yoti.com/CAEaJDQzNzllZDc0LTU0YjItNDkxMy04OTE4LTExYzM2ZDU2OTU3ZDAC +} + +func TestCreateShareURL_Unsuccessful_503(t *testing.T) { + _, err := createShareUrlWithErrorResponse(503, "some service unavailable response") + + assert.ErrorContains(t, err, "503: unknown HTTP error - some service unavailable response") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, temporary && tempError.Temporary()) +} + +func TestCreateShareURL_Unsuccessful_404(t *testing.T) { + _, err := createShareUrlWithErrorResponse(404, "some not found response") + + assert.ErrorContains(t, err, "404: Application was not found - some not found response") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestCreateShareURL_Unsuccessful_400(t *testing.T) { + _, err := createShareUrlWithErrorResponse(400, "some invalid JSON response") + + assert.ErrorContains(t, err, "400: JSON is incorrect, contains invalid data - some invalid JSON response") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func createShareUrlWithErrorResponse(statusCode int, responseBody string) (share ShareURL, err error) { + key := test.GetValidKey("../test/test-key.pem") + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(responseBody)), + }, nil + }, + } + + policy, err := (&PolicyBuilder{}).WithFullName().WithWantedRememberMe().Build() + if err != nil { + return + } + scenario, err := (&ScenarioBuilder{}).WithPolicy(policy).Build() + if err != nil { + return + } + + return CreateShareURL(client, &scenario, "sdkId", "https://apiurl", key) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/share_url.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/share_url.go new file mode 100644 index 0000000..3b66507 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/share_url.go @@ -0,0 +1,16 @@ +package dynamic + +var ( + // ShareURLHTTPErrorMessages specifies the HTTP error status codes used + // by the Share URL API + ShareURLHTTPErrorMessages = map[int]string{ + 400: "JSON is incorrect, contains invalid data", + 404: "Application was not found", + } +) + +// ShareURL contains a dynamic share QR code +type ShareURL struct { + ShareURL string `json:"qrcode"` + RefID string `json:"ref_id"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/source_constraint.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/source_constraint.go new file mode 100644 index 0000000..511b703 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/source_constraint.go @@ -0,0 +1,105 @@ +package dynamic + +import ( + "encoding/json" + + "github.com/getyoti/yoti-go-sdk/v3/yotierror" +) + +// Anchor name constants +const ( + AnchorDrivingLicenceConst = "DRIVING_LICENCE" + AnchorPassportConst = "PASSPORT" + AnchorNationalIDConst = "NATIONAL_ID" + AnchorPassCardConst = "PASS_CARD" +) + +// SourceConstraint describes a requirement or preference for a particular set +// of anchors +type SourceConstraint struct { + anchors []WantedAnchor + softPreference bool +} + +// SourceConstraintBuilder builds a source constraint +type SourceConstraintBuilder struct { + sourceConstraint SourceConstraint + err error +} + +// WithAnchorByValue is a helper method which builds an anchor and adds it to +// the source constraint +func (b *SourceConstraintBuilder) WithAnchorByValue(value, subtype string) *SourceConstraintBuilder { + anchor, err := (&WantedAnchorBuilder{}). + WithValue(value). + WithSubType(subtype). + Build() + if err != nil { + b.err = yotierror.MultiError{This: err, Next: b.err} + } + + return b.WithAnchor(anchor) +} + +// WithAnchor adds an anchor to the preference list +func (b *SourceConstraintBuilder) WithAnchor(anchor WantedAnchor) *SourceConstraintBuilder { + b.sourceConstraint.anchors = append(b.sourceConstraint.anchors, anchor) + return b +} + +// WithPassport adds a passport anchor +func (b *SourceConstraintBuilder) WithPassport(subtype string) *SourceConstraintBuilder { + return b.WithAnchorByValue(AnchorPassportConst, subtype) +} + +// WithDrivingLicence adds a Driving Licence anchor +func (b *SourceConstraintBuilder) WithDrivingLicence(subtype string) *SourceConstraintBuilder { + return b.WithAnchorByValue(AnchorDrivingLicenceConst, subtype) +} + +// WithNationalID adds a national ID anchor +func (b *SourceConstraintBuilder) WithNationalID(subtype string) *SourceConstraintBuilder { + return b.WithAnchorByValue(AnchorNationalIDConst, subtype) +} + +// WithPasscard adds a passcard anchor +func (b *SourceConstraintBuilder) WithPasscard(subtype string) *SourceConstraintBuilder { + return b.WithAnchorByValue(AnchorPassCardConst, subtype) +} + +// WithSoftPreference sets this constraint as a 'soft requirement' if the +// parameter is true, and a hard requirement if it is false. +func (b *SourceConstraintBuilder) WithSoftPreference(soft bool) *SourceConstraintBuilder { + b.sourceConstraint.softPreference = soft + return b +} + +// Build builds a SourceConstraint +func (b *SourceConstraintBuilder) Build() (SourceConstraint, error) { + if b.sourceConstraint.anchors == nil { + b.sourceConstraint.anchors = make([]WantedAnchor, 0) + } + return b.sourceConstraint, b.err +} + +func (constraint *SourceConstraint) isConstraint() bool { + return true +} + +// MarshalJSON returns the JSON encoding +func (constraint *SourceConstraint) MarshalJSON() ([]byte, error) { + type PreferenceList struct { + Anchors []WantedAnchor `json:"anchors"` + SoftPreference bool `json:"soft_preference"` + } + return json.Marshal(&struct { + Type string `json:"type"` + PreferredSources PreferenceList `json:"preferred_sources"` + }{ + Type: "SOURCE", + PreferredSources: PreferenceList{ + Anchors: constraint.anchors, + SoftPreference: constraint.softPreference, + }, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/source_constraint_builder_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/source_constraint_builder_test.go new file mode 100644 index 0000000..6b8f39f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/source_constraint_builder_test.go @@ -0,0 +1,115 @@ +package dynamic + +import ( + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleSourceConstraint() { + drivingLicence, err := (&WantedAnchorBuilder{}).WithValue("DRIVING_LICENCE").Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + sourceConstraint, err := (&SourceConstraintBuilder{}). + WithAnchor(drivingLicence). + WithSoftPreference(true). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + json, err := sourceConstraint.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println("SourceConstraint:", string(json)) + // Output: SourceConstraint: {"type":"SOURCE","preferred_sources":{"anchors":[{"name":"DRIVING_LICENCE","sub_type":""}],"soft_preference":true}} +} + +func ExampleSourceConstraintBuilder_WithPassport() { + sourceConstraint, err := (&SourceConstraintBuilder{}). + WithPassport(""). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + json, err := sourceConstraint.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(json)) + // Output: {"type":"SOURCE","preferred_sources":{"anchors":[{"name":"PASSPORT","sub_type":""}],"soft_preference":false}} +} + +func ExampleSourceConstraintBuilder_WithDrivingLicence() { + sourceConstraint, err := (&SourceConstraintBuilder{}). + WithDrivingLicence(""). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + json, err := sourceConstraint.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(json)) + // Output: {"type":"SOURCE","preferred_sources":{"anchors":[{"name":"DRIVING_LICENCE","sub_type":""}],"soft_preference":false}} +} + +func ExampleSourceConstraintBuilder_WithNationalID() { + sourceConstraint, err := (&SourceConstraintBuilder{}). + WithNationalID(""). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + json, err := sourceConstraint.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(json)) + // Output: {"type":"SOURCE","preferred_sources":{"anchors":[{"name":"NATIONAL_ID","sub_type":""}],"soft_preference":false}} +} + +func ExampleSourceConstraintBuilder_WithPasscard() { + sourceConstraint, err := (&SourceConstraintBuilder{}). + WithPasscard(""). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + json, err := sourceConstraint.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(json)) + // Output: {"type":"SOURCE","preferred_sources":{"anchors":[{"name":"PASS_CARD","sub_type":""}],"soft_preference":false}} +} + +func TestSourceConstraint_isConstraintImplemented(t *testing.T) { + constraint := &SourceConstraint{} + assert.Check(t, constraint.isConstraint()) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_anchor_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_anchor_builder.go new file mode 100644 index 0000000..1a5c7b6 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_anchor_builder.go @@ -0,0 +1,44 @@ +package dynamic + +import ( + "encoding/json" +) + +// WantedAnchor specifies a preferred anchor for a user's details +type WantedAnchor struct { + name string + subType string +} + +// WantedAnchorBuilder describes a desired anchor for user profile data +type WantedAnchorBuilder struct { + wantedAnchor WantedAnchor +} + +// WithValue sets the anchor's name +func (b *WantedAnchorBuilder) WithValue(name string) *WantedAnchorBuilder { + b.wantedAnchor.name = name + return b +} + +// WithSubType sets the anchors subtype +func (b *WantedAnchorBuilder) WithSubType(subType string) *WantedAnchorBuilder { + b.wantedAnchor.subType = subType + return b +} + +// Build constructs the anchor from the builder's specification +func (b *WantedAnchorBuilder) Build() (WantedAnchor, error) { + return b.wantedAnchor, nil +} + +// MarshalJSON ... +func (a *WantedAnchor) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Name string `json:"name"` + SubType string `json:"sub_type"` + }{ + Name: a.name, + SubType: a.subType, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_anchor_builder_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_anchor_builder_test.go new file mode 100644 index 0000000..2d9e74d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_anchor_builder_test.go @@ -0,0 +1,25 @@ +package dynamic + +import ( + "fmt" +) + +func ExampleWantedAnchorBuilder() { + aadhaarAnchor, err := (&WantedAnchorBuilder{}). + WithValue("NATIONAL_ID"). + WithSubType("AADHAAR"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + aadhaarJSON, err := aadhaarAnchor.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println("Aadhaar:", string(aadhaarJSON)) + // Output: Aadhaar: {"name":"NATIONAL_ID","sub_type":"AADHAAR"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_attribute_builder.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_attribute_builder.go new file mode 100644 index 0000000..bc2c76f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_attribute_builder.go @@ -0,0 +1,81 @@ +package dynamic + +import ( + "encoding/json" + "errors" +) + +type constraintInterface interface { + MarshalJSON() ([]byte, error) + isConstraint() bool // This function is not used but makes inheritance explicit +} + +// WantedAttributeBuilder generates the payload for specifying a single wanted +// attribute as part of a dynamic scenario +type WantedAttributeBuilder struct { + attr WantedAttribute +} + +// WantedAttribute represents a wanted attribute in a dynamic sharing policy +type WantedAttribute struct { + name string + derivation string + constraints []constraintInterface + acceptSelfAsserted bool + Optional bool +} + +// WithName sets the name of the wanted attribute +func (builder *WantedAttributeBuilder) WithName(name string) *WantedAttributeBuilder { + builder.attr.name = name + return builder +} + +// WithDerivation sets the derivation +func (builder *WantedAttributeBuilder) WithDerivation(derivation string) *WantedAttributeBuilder { + builder.attr.derivation = derivation + return builder +} + +// WithConstraint adds a constraint to a wanted attribute +func (builder *WantedAttributeBuilder) WithConstraint(constraint constraintInterface) *WantedAttributeBuilder { + builder.attr.constraints = append(builder.attr.constraints, constraint) + return builder +} + +// WithAcceptSelfAsserted allows self-asserted user details, such as those from Aadhar +func (builder *WantedAttributeBuilder) WithAcceptSelfAsserted(accept bool) *WantedAttributeBuilder { + builder.attr.acceptSelfAsserted = accept + return builder +} + +// Build generates the wanted attribute's specification +func (builder *WantedAttributeBuilder) Build() (WantedAttribute, error) { + if builder.attr.constraints == nil { + builder.attr.constraints = make([]constraintInterface, 0) + } + + var err error + if len(builder.attr.name) == 0 { + err = errors.New("Wanted attribute names must not be empty") + } + + return builder.attr, err +} + +// MarshalJSON returns the JSON encoding +func (attr *WantedAttribute) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Name string `json:"name"` + Derivation string `json:"derivation,omitempty"` + Constraints []constraintInterface `json:"constraints,omitempty"` + AcceptSelfAsserted bool `json:"accept_self_asserted"` + Optional bool `json:"optional,omitempty"` + }{ + Name: attr.name, + Derivation: attr.derivation, + Constraints: attr.constraints, + AcceptSelfAsserted: attr.acceptSelfAsserted, + Optional: attr.Optional, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_attribute_test.go new file mode 100644 index 0000000..43d189d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/dynamic/wanted_attribute_test.go @@ -0,0 +1,154 @@ +package dynamic + +import ( + "encoding/json" + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func ExampleWantedAttributeBuilder_WithName() { + builder := (&WantedAttributeBuilder{}).WithName("TEST NAME") + attribute, err := builder.Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(attribute.name) + // Output: TEST NAME +} + +func ExampleWantedAttributeBuilder_WithDerivation() { + attribute, err := (&WantedAttributeBuilder{}). + WithDerivation("TEST DERIVATION"). + WithName("TEST NAME"). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(attribute.derivation) + // Output: TEST DERIVATION +} + +func ExampleWantedAttributeBuilder_WithConstraint() { + constraint, err := (&SourceConstraintBuilder{}). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + WithConstraint(&constraint). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"TEST NAME","constraints":[{"type":"SOURCE","preferred_sources":{"anchors":[],"soft_preference":false}}],"accept_self_asserted":false} +} + +func ExampleWantedAttributeBuilder_WithAcceptSelfAsserted() { + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + WithAcceptSelfAsserted(true). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"TEST NAME","accept_self_asserted":true} +} + +func ExampleWantedAttributeBuilder_WithAcceptSelfAsserted_false() { + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + WithAcceptSelfAsserted(false). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"TEST NAME","accept_self_asserted":false} +} + +func ExampleWantedAttributeBuilder_optional_true() { + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + WithAcceptSelfAsserted(false). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + attribute.Optional = true + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"TEST NAME","accept_self_asserted":false,"optional":true} +} + +func TestWantedAttributeBuilder_Optional_IsOmittedByDefault(t *testing.T) { + attribute, err := (&WantedAttributeBuilder{}). + WithName("TEST NAME"). + Build() + if err != nil { + t.Errorf("error: %s", err.Error()) + } + + marshalledJSON, err := attribute.MarshalJSON() + if err != nil { + t.Errorf("error: %s", err.Error()) + } + + attributeMap := unmarshalJSONIntoMap(t, marshalledJSON) + + optional := attributeMap["optional"] + + if optional != nil { + t.Errorf("expected `optional` to be nil, but was: '%v'", optional) + } +} + +func unmarshalJSONIntoMap(t *testing.T, byteValue []byte) (result map[string]interface{}) { + var unmarshalled interface{} + err := json.Unmarshal(byteValue, &unmarshalled) + assert.NilError(t, err) + + return unmarshalled.(map[string]interface{}) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/extension.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/extension.go new file mode 100644 index 0000000..dc57fbb --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/extension.go @@ -0,0 +1,46 @@ +package extension + +import ( + "encoding/json" +) + +// Extension is a generic type of extension that can be used where a more +// specialised Extension type is not available +type Extension struct { + extensionType string + content interface{} +} + +// Builder is used to construct an Extension object +type Builder struct { + extension Extension +} + +// WithType sets the extension type string +func (builder *Builder) WithType(extensionType string) *Builder { + builder.extension.extensionType = extensionType + return builder +} + +// WithContent attaches data to the Extension. The content must implement JSON +// serialization +func (builder *Builder) WithContent(content interface{}) *Builder { + builder.extension.content = content + return builder +} + +// Build constructs the Extension +func (builder *Builder) Build() (Extension, error) { + return builder.extension, nil +} + +// MarshalJSON returns the JSON encoding +func (extension Extension) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Content interface{} `json:"content"` + }{ + Type: extension.extensionType, + Content: extension.content, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/extension_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/extension_test.go new file mode 100644 index 0000000..5ac07e7 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/extension_test.go @@ -0,0 +1,24 @@ +package extension + +import ( + "fmt" +) + +func ExampleExtension() { + content := "SOME CONTENT" + extType := "SOME_TYPE" + extension, err := (&Builder{}).WithContent(content).WithType(extType).Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := extension.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"SOME_TYPE","content":"SOME CONTENT"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/location_constraint_extension.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/location_constraint_extension.go new file mode 100644 index 0000000..6120b3b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/location_constraint_extension.go @@ -0,0 +1,78 @@ +package extension + +import ( + "encoding/json" +) + +const ( + locationConstraintExtensionTypeConst = "LOCATION_CONSTRAINT" +) + +// LocationConstraintExtensionBuilder is used to construct a LocationConstraintExtension +type LocationConstraintExtensionBuilder struct { + extension LocationConstraintExtension +} + +// LocationConstraintExtension is an extension representing a geographic constraint +type LocationConstraintExtension struct { + latitude float64 + longitude float64 + radius float64 + uncertainty float64 +} + +// WithLatitude sets the latitude of the location constraint +func (builder *LocationConstraintExtensionBuilder) WithLatitude(latitude float64) *LocationConstraintExtensionBuilder { + builder.extension.latitude = latitude + return builder +} + +// WithLongitude sets the longitude of the location constraint +func (builder *LocationConstraintExtensionBuilder) WithLongitude(longitude float64) *LocationConstraintExtensionBuilder { + builder.extension.longitude = longitude + return builder +} + +// WithRadius sets the radius within which the location constraint will be satisfied +func (builder *LocationConstraintExtensionBuilder) WithRadius(radius float64) *LocationConstraintExtensionBuilder { + builder.extension.radius = radius + return builder +} + +// WithMaxUncertainty sets the max uncertainty allowed by the location constraint extension +func (builder *LocationConstraintExtensionBuilder) WithMaxUncertainty(uncertainty float64) *LocationConstraintExtensionBuilder { + builder.extension.uncertainty = uncertainty + return builder +} + +// Build constructs a LocationConstraintExtension from the builder +func (builder *LocationConstraintExtensionBuilder) Build() (LocationConstraintExtension, error) { + return builder.extension, nil +} + +// MarshalJSON returns the JSON encoding +func (extension LocationConstraintExtension) MarshalJSON() ([]byte, error) { + type location struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Radius float64 `json:"radius"` + MaxUncertainty float64 `json:"max_uncertainty_radius"` + } + type content struct { + Location location `json:"expected_device_location"` + } + return json.Marshal(&struct { + Type string `json:"type"` + Content content `json:"content"` + }{ + Type: locationConstraintExtensionTypeConst, + Content: content{ + Location: location{ + Latitude: extension.latitude, + Longitude: extension.longitude, + Radius: extension.radius, + MaxUncertainty: extension.uncertainty, + }, + }, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/location_constraint_extension_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/location_constraint_extension_test.go new file mode 100644 index 0000000..05e1621 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/location_constraint_extension_test.go @@ -0,0 +1,27 @@ +package extension + +import ( + "fmt" +) + +func ExampleLocationConstraintExtension() { + extension, err := (&LocationConstraintExtensionBuilder{}). + WithLatitude(51.511831). + WithLongitude(-0.081446). + WithRadius(0.001). + WithMaxUncertainty(0.001). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := extension.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"LOCATION_CONSTRAINT","content":{"expected_device_location":{"latitude":51.511831,"longitude":-0.081446,"radius":0.001,"max_uncertainty_radius":0.001}}} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/third_party_attribute_extension.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/third_party_attribute_extension.go new file mode 100644 index 0000000..d169424 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/third_party_attribute_extension.go @@ -0,0 +1,64 @@ +package extension + +import ( + "encoding/json" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" +) + +const ( + thirdPartyAttributeExtensionTypeConst = "THIRD_PARTY_ATTRIBUTE" +) + +// ThirdPartyAttributeExtensionBuilder is used to construct a ThirdPartyAttributeExtension +type ThirdPartyAttributeExtensionBuilder struct { + extension ThirdPartyAttributeExtension +} + +// ThirdPartyAttributeExtension is an extension representing the issuance of a third party attribute +type ThirdPartyAttributeExtension struct { + expiryDate *time.Time + definitions []attribute.Definition +} + +// WithExpiryDate sets the expiry date of the extension as a UTC timestamp +func (builder *ThirdPartyAttributeExtensionBuilder) WithExpiryDate(expiryDate *time.Time) *ThirdPartyAttributeExtensionBuilder { + builder.extension.expiryDate = expiryDate + return builder +} + +// WithDefinition adds an attribute.AttributeDefinition to the list of definitions +func (builder *ThirdPartyAttributeExtensionBuilder) WithDefinition(definition attribute.Definition) *ThirdPartyAttributeExtensionBuilder { + builder.extension.definitions = append(builder.extension.definitions, definition) + return builder +} + +// WithDefinitions sets the array of attribute.AttributeDefinition on the extension +func (builder *ThirdPartyAttributeExtensionBuilder) WithDefinitions(definitions []attribute.Definition) *ThirdPartyAttributeExtensionBuilder { + builder.extension.definitions = definitions + return builder +} + +// Build creates a ThirdPartyAttributeExtension using the supplied values +func (builder *ThirdPartyAttributeExtensionBuilder) Build() (ThirdPartyAttributeExtension, error) { + return builder.extension, nil +} + +// MarshalJSON returns the JSON encoding +func (extension ThirdPartyAttributeExtension) MarshalJSON() ([]byte, error) { + type thirdPartyAttributeExtension struct { + ExpiryDate string `json:"expiry_date"` + Definitions []attribute.Definition `json:"definitions"` + } + return json.Marshal(&struct { + Type string `json:"type"` + Content thirdPartyAttributeExtension `json:"content"` + }{ + Type: thirdPartyAttributeExtensionTypeConst, + Content: thirdPartyAttributeExtension{ + ExpiryDate: extension.expiryDate.UTC().Format("2006-01-02T15:04:05.000Z"), + Definitions: extension.definitions, + }, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/third_party_attribute_extension_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/third_party_attribute_extension_test.go new file mode 100644 index 0000000..e51f661 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/third_party_attribute_extension_test.go @@ -0,0 +1,142 @@ +package extension + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "gotest.tools/v3/assert" +) + +func createDefinitionByName(name string) attribute.Definition { + return attribute.NewAttributeDefinition(name) +} + +func ExampleThirdPartyAttributeExtension() { + attributeDefinition := attribute.NewAttributeDefinition("some_value") + + datetime, err := time.Parse("2006-01-02T15:04:05.000Z", "2019-10-30T12:10:09.458Z") + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + extension, err := (&ThirdPartyAttributeExtensionBuilder{}). + WithExpiryDate(&datetime). + WithDefinition(attributeDefinition). + Build() + + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := extension.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"THIRD_PARTY_ATTRIBUTE","content":{"expiry_date":"2019-10-30T12:10:09.458Z","definitions":[{"name":"some_value"}]}} +} + +func TestWithDefinitionShouldAddToList(t *testing.T) { + datetime, err := time.Parse("2006-01-02T15:04:05.000Z", "2019-10-30T12:10:09.458Z") + assert.NilError(t, err) + + definitionList := []attribute.Definition{ + createDefinitionByName("some_attribute"), + createDefinitionByName("some_other_attribute"), + } + + someOtherDefinition := createDefinitionByName("wanted_definition") + + extension, err := (&ThirdPartyAttributeExtensionBuilder{}). + WithExpiryDate(&datetime). + WithDefinitions(definitionList). + WithDefinition(someOtherDefinition). + Build() + + assert.NilError(t, err) + assert.Equal(t, len(extension.definitions), 3) + assert.Equal(t, extension.definitions[0].Name(), "some_attribute") + assert.Equal(t, extension.definitions[1].Name(), "some_other_attribute") + assert.Equal(t, extension.definitions[2].Name(), "wanted_definition") +} + +func TestWithDefinitionsShouldOverwriteList(t *testing.T) { + datetime, err := time.Parse("2006-01-02T15:04:05.000Z", "2019-10-30T12:10:09.458Z") + assert.NilError(t, err) + + definitionList := []attribute.Definition{ + createDefinitionByName("some_attribute"), + createDefinitionByName("some_other_attribute"), + } + + someOtherDefinition := createDefinitionByName("wanted_definition") + + extension, err := (&ThirdPartyAttributeExtensionBuilder{}). + WithExpiryDate(&datetime). + WithDefinition(someOtherDefinition). + WithDefinitions(definitionList). + Build() + + assert.NilError(t, err) + assert.Equal(t, len(extension.definitions), 2) + assert.Equal(t, extension.definitions[0].Name(), "some_attribute") + assert.Equal(t, extension.definitions[1].Name(), "some_other_attribute") +} + +var expiryDates = []struct { + in time.Time + expected string +}{ + {time.Date(2051, 01, 13, 19, 50, 53, 1, time.UTC), "2051-01-13T19:50:53.000Z"}, + {time.Date(2026, 02, 02, 22, 04, 05, 123, time.UTC), "2026-02-02T22:04:05.000Z"}, + {time.Date(2051, 03, 13, 19, 50, 53, 9999, time.UTC), "2051-03-13T19:50:53.000Z"}, + {time.Date(2051, 04, 13, 19, 50, 53, 999999, time.UTC), "2051-04-13T19:50:53.000Z"}, + {time.Date(2026, 01, 31, 22, 04, 05, 1232567, time.UTC), "2026-01-31T22:04:05.001Z"}, + {time.Date(2026, 01, 31, 22, 04, 05, 17777777, time.UTC), "2026-01-31T22:04:05.017Z"}, + {time.Date(2026, 07, 31, 22, 04, 05, 000777777, time.UTC), "2026-07-31T22:04:05.000Z"}, + {time.Date(2026, 01, 02, 22, 04, 05, 123456789, time.UTC), "2026-01-02T22:04:05.123Z"}, + {time.Date(2028, 10, 02, 10, 00, 00, 0, time.FixedZone("UTC-5", -5*60*60)), "2028-10-02T15:00:00.000Z"}, + {time.Date(2028, 10, 02, 10, 00, 00, 0, time.FixedZone("UTC+11", 11*60*60)), "2028-10-01T23:00:00.000Z"}, + {time.Unix(1734567899, 0), "2024-12-19T00:24:59.000Z"}, + {time.Unix(2234567891, 0), "2040-10-23T01:18:11.000Z"}, +} + +func TestExpiryDatesAreFormattedCorrectly(t *testing.T) { + attributeDefinition := attribute.NewAttributeDefinition("some_value") + + for _, date := range expiryDates { + extension, err := (&ThirdPartyAttributeExtensionBuilder{}). + WithExpiryDate(&date.in). + WithDefinition(attributeDefinition). + Build() + + assert.NilError(t, err) + + marshalledJson, err := extension.MarshalJSON() + assert.NilError(t, err) + + attributeIssuanceDetailsJson := unmarshalJSONIntoMap(t, marshalledJson) + + content := attributeIssuanceDetailsJson["content"].(map[string]interface{}) + result := content["expiry_date"] + + if result != date.expected { + t.Errorf("got %q, want %q", result, date.expected) + } + } +} + +func unmarshalJSONIntoMap(t *testing.T, byteValue []byte) (result map[string]interface{}) { + var unmarshalled interface{} + err := json.Unmarshal(byteValue, &unmarshalled) + assert.NilError(t, err) + + return unmarshalled.(map[string]interface{}) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/transactional_flow_extension.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/transactional_flow_extension.go new file mode 100644 index 0000000..1618c5e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/transactional_flow_extension.go @@ -0,0 +1,42 @@ +package extension + +import ( + "encoding/json" +) + +const ( + transactionalFlowExtensionTypeConst = "TRANSACTIONAL_FLOW" +) + +// TransactionalFlowExtension represents a type of extension in a dynamic share +type TransactionalFlowExtension struct { + content interface{} +} + +// TransactionalFlowExtensionBuilder constructs a TransactionalFlowExtension +type TransactionalFlowExtensionBuilder struct { + extension TransactionalFlowExtension +} + +// WithContent sets the payload data for a TransactionalFlowExtension. The +// content must implement JSON serialization +func (builder *TransactionalFlowExtensionBuilder) WithContent(content interface{}) *TransactionalFlowExtensionBuilder { + builder.extension.content = content + return builder +} + +// Build constructs a TransactionalFlowExtension +func (builder *TransactionalFlowExtensionBuilder) Build() (TransactionalFlowExtension, error) { + return builder.extension, nil +} + +// MarshalJSON returns the JSON encoding +func (extension TransactionalFlowExtension) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Content interface{} `json:"content"` + }{ + Type: transactionalFlowExtensionTypeConst, + Content: extension.content, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/transactional_flow_extension_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/transactional_flow_extension_test.go new file mode 100644 index 0000000..cda4e36 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extension/transactional_flow_extension_test.go @@ -0,0 +1,27 @@ +package extension + +import ( + "encoding/json" + "fmt" +) + +func ExampleTransactionalFlowExtension() { + content := "SOME CONTENT" + + extension, err := (&TransactionalFlowExtensionBuilder{}). + WithContent(content). + Build() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + data, err := json.Marshal(extension) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(data)) + // Output: {"type":"TRANSACTIONAL_FLOW","content":"SOME CONTENT"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extra/extra_data.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extra/extra_data.go new file mode 100644 index 0000000..b61ba4e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extra/extra_data.go @@ -0,0 +1,50 @@ +package extra + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare" + "google.golang.org/protobuf/proto" +) + +// Data represents extra pieces information on the receipt. +// Initialize with NewExtraData or DefaultExtraData +type Data struct { + attributeIssuanceDetails *attribute.IssuanceDetails +} + +// DefaultExtraData initialises the ExtraData struct +func DefaultExtraData() (extraData *Data) { + return &Data{ + attributeIssuanceDetails: nil, + } +} + +// NewExtraData takes a base64 encoded string and parses it into ExtraData +func NewExtraData(extraDataBytes []byte) (*Data, error) { + var err error + var extraData = DefaultExtraData() + + extraDataProto := &yotiprotoshare.ExtraData{} + if err = proto.Unmarshal(extraDataBytes, extraDataProto); err != nil { + return extraData, err + } + + var attributeIssuanceDetails *attribute.IssuanceDetails + + for _, de := range extraDataProto.GetList() { + if de.Type == yotiprotoshare.DataEntry_THIRD_PARTY_ATTRIBUTE { + attributeIssuanceDetails, err = attribute.ParseIssuanceDetails(de.Value) + + return &Data{ + attributeIssuanceDetails: attributeIssuanceDetails, + }, err + } + } + + return extraData, nil +} + +// AttributeIssuanceDetails represents the details of attribute(s) to be issued by a third party. Will be nil if not provided by Yoti. +func (e Data) AttributeIssuanceDetails() (issuanceDetails *attribute.IssuanceDetails) { + return e.attributeIssuanceDetails +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extra/extra_data_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extra/extra_data_test.go new file mode 100644 index 0000000..816dfaa --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/extra/extra_data_test.go @@ -0,0 +1,153 @@ +package extra + +import ( + "encoding/base64" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/test" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare" + "google.golang.org/protobuf/proto" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestAttributeIssuanceDetailsShouldReturnNilWhenNoDataEntries(t *testing.T) { + extraData := DefaultExtraData() + + issuanceDetails := extraData.AttributeIssuanceDetails() + + assert.Assert(t, is.Nil(issuanceDetails)) +} + +func TestShouldReturnFirstMatchingThirdPartyAttribute(t *testing.T) { + dataEntries := make([]*yotiprotoshare.DataEntry, 0) + + expiryDate := time.Now().UTC().AddDate(0, 0, 1) + var tokenValue1 = "tokenValue1" + + thirdPartyAttributeDataEntry1 := test.CreateThirdPartyAttributeDataEntry(t, &expiryDate, []string{"attributeName1"}, tokenValue1) + thirdPartyAttributeDataEntry2 := test.CreateThirdPartyAttributeDataEntry(t, &expiryDate, []string{"attributeName2"}, "tokenValue2") + + dataEntries = append(dataEntries, &thirdPartyAttributeDataEntry1, &thirdPartyAttributeDataEntry2) + protoExtraData := &yotiprotoshare.ExtraData{ + List: dataEntries, + } + + parsedExtraData, err := parseProtoExtraData(t, protoExtraData) + assert.NilError(t, err) + + result := parsedExtraData.AttributeIssuanceDetails() + + var tokenBytes = []byte(tokenValue1) + var base64EncodedToken = base64.StdEncoding.EncodeToString(tokenBytes) + + assert.Equal(t, result.Token(), base64EncodedToken) + assert.Equal(t, result.Attributes()[0].Name(), "attributeName1") + assert.Equal(t, + result.ExpiryDate().Format("2006-01-02T15:04:05.000Z"), + expiryDate.Format("2006-01-02T15:04:05.000Z")) +} + +func TestShouldParseMultipleIssuingAttributes(t *testing.T) { + var base64ExtraData = test.GetTestFileAsString(t, "../test/fixtures/test_extra_data.txt") + rawExtraData, err := base64.StdEncoding.DecodeString(base64ExtraData) + assert.NilError(t, err) + + extraData, err := NewExtraData(rawExtraData) + assert.NilError(t, err) + + result := extraData.AttributeIssuanceDetails() + + assert.Equal(t, result.Token(), "c29tZUlzc3VhbmNlVG9rZW4=") + assert.Equal(t, + result.ExpiryDate().Format("2006-01-02T15:04:05.000Z"), + time.Date(2019, time.October, 15, 22, 04, 05, 123000000, time.UTC).Format("2006-01-02T15:04:05.000Z")) + assert.Equal(t, result.Attributes()[0].Name(), "com.thirdparty.id") + assert.Equal(t, result.Attributes()[1].Name(), "com.thirdparty.other_id") +} + +func TestShouldHandleNoExpiryDate(t *testing.T) { + var protoDefinitions []*yotiprotoshare.Definition + + protoDefinitions = append(protoDefinitions, &yotiprotoshare.Definition{Name: "attribute.name"}) + + thirdPartyAttribute := &yotiprotoshare.ThirdPartyAttribute{ + IssuanceToken: []byte("tokenValue"), + IssuingAttributes: &yotiprotoshare.IssuingAttributes{ + ExpiryDate: "", + Definitions: protoDefinitions, + }, + } + + marshalledThirdPartyAttribute, err := proto.Marshal(thirdPartyAttribute) + assert.NilError(t, err) + + result, err := processThirdPartyAttribute(t, marshalledThirdPartyAttribute) + assert.NilError(t, err) + + assert.Assert(t, is.Nil(result.ExpiryDate())) +} + +func TestShouldHandleNoIssuingAttributes(t *testing.T) { + var tokenValueBytes = []byte("token") + thirdPartyAttribute := &yotiprotoshare.ThirdPartyAttribute{ + IssuanceToken: tokenValueBytes, + IssuingAttributes: &yotiprotoshare.IssuingAttributes{}, + } + + marshalledThirdPartyAttribute, err := proto.Marshal(thirdPartyAttribute) + assert.NilError(t, err) + + result, err := processThirdPartyAttribute(t, marshalledThirdPartyAttribute) + + assert.NilError(t, err) + assert.Equal(t, base64.StdEncoding.EncodeToString(tokenValueBytes), result.Token()) +} + +func TestShouldHandleNoIssuingAttributeDefinitions(t *testing.T) { + var tokenValueBytes = []byte("token") + + thirdPartyAttribute := &yotiprotoshare.ThirdPartyAttribute{ + IssuanceToken: tokenValueBytes, + IssuingAttributes: &yotiprotoshare.IssuingAttributes{ + ExpiryDate: time.Now().UTC().AddDate(0, 0, 1).Format("2006-01-02T15:04:05.000Z"), + Definitions: []*yotiprotoshare.Definition{}, + }, + } + + marshalledThirdPartyAttribute, err := proto.Marshal(thirdPartyAttribute) + assert.NilError(t, err) + + result, err := processThirdPartyAttribute(t, marshalledThirdPartyAttribute) + + assert.NilError(t, err) + assert.Equal(t, base64.StdEncoding.EncodeToString(tokenValueBytes), result.Token()) +} + +func processThirdPartyAttribute(t *testing.T, marshalledThirdPartyAttribute []byte) (*attribute.IssuanceDetails, error) { + dataEntries := make([]*yotiprotoshare.DataEntry, 0) + + thirdPartyAttributeDataEntry := yotiprotoshare.DataEntry{ + Type: yotiprotoshare.DataEntry_THIRD_PARTY_ATTRIBUTE, + Value: marshalledThirdPartyAttribute, + } + + dataEntries = append(dataEntries, &thirdPartyAttributeDataEntry) + protoExtraData := &yotiprotoshare.ExtraData{ + List: dataEntries, + } + + parsedExtraData, err := parseProtoExtraData(t, protoExtraData) + + return parsedExtraData.AttributeIssuanceDetails(), err +} + +func parseProtoExtraData(t *testing.T, protoExtraData *yotiprotoshare.ExtraData) (*Data, error) { + extraDataMarshalled, err := proto.Marshal(protoExtraData) + assert.NilError(t, err) + + return NewExtraData(extraDataMarshalled) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/file/file.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/file/file.go new file mode 100644 index 0000000..a002b96 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/file/file.go @@ -0,0 +1,21 @@ +package file + +import ( + "io" + "os" +) + +// ReadFile reads a file until an error or EOF +func ReadFile(filename string) ([]byte, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + + buffer, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + return buffer, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/file/file_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/file/file_test.go new file mode 100644 index 0000000..cb68adb --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/file/file_test.go @@ -0,0 +1,18 @@ +package file + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestFile_ReadFile(t *testing.T) { + _, err := ReadFile("../test/test-key.pem") + assert.NilError(t, err) +} + +func TestFile_ReadFile_ShouldFailForFileNotFound(t *testing.T) { + MissingFileName := "/tmp/file_not_found" + _, err := ReadFile(MissingFileName) + assert.Check(t, err != nil) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/go.mod b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/go.mod new file mode 100644 index 0000000..c1c87ab --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/go.mod @@ -0,0 +1,10 @@ +module github.com/getyoti/yoti-go-sdk/v3 + +require ( + google.golang.org/protobuf v1.28.0 + gotest.tools/v3 v3.3.0 +) + +require github.com/google/go-cmp v0.5.5 // indirect + +go 1.19 diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/go.sum b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/go.sum new file mode 100644 index 0000000..7b99939 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/go.sum @@ -0,0 +1,33 @@ +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/image.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/image.go new file mode 100644 index 0000000..35796b5 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/image.go @@ -0,0 +1,45 @@ +package media + +const ( + // ImageTypeJPEG JPEG format + ImageTypeJPEG string = "image/jpeg" + + // ImageTypePNG PNG format + ImageTypePNG string = "image/png" +) + +// PNGImage holds the binary data of a PNG image. +type PNGImage []byte + +// Base64URL is PNG image encoded as a base64 URL. +func (i PNGImage) Base64URL() string { + return base64URL(i.MIME(), i) +} + +// MIME returns the MIME type for PNG images. +func (PNGImage) MIME() string { + return ImageTypePNG +} + +// Data returns the PNG image as raw bytes. +func (i PNGImage) Data() []byte { + return i +} + +// JPEGImage holds the binary data of a JPEG image. +type JPEGImage []byte + +// Base64URL is JPEG image encoded as a base64 URL. +func (i JPEGImage) Base64URL() string { + return base64URL(i.MIME(), i) +} + +// MIME returns the MIME type for JPEG images. +func (JPEGImage) MIME() string { + return ImageTypeJPEG +} + +// Data returns the JPEG image as raw bytes. +func (i JPEGImage) Data() []byte { + return i +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/image_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/image_test.go new file mode 100644 index 0000000..85f162c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/image_test.go @@ -0,0 +1,31 @@ +package media + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +const ( + imageBase64Value = "dmFsdWU=" +) + +func TestImage_Base64URL_CreateJpegImage(t *testing.T) { + imageBytes := []byte("value") + + result := JPEGImage(imageBytes) + expectedDataURL := "data:image/jpeg;base64," + imageBase64Value + + assert.Equal(t, expectedDataURL, result.Base64URL()) + assert.DeepEqual(t, imageBytes, result.Data()) +} + +func TestImage_Base64URL_CreatePngImage(t *testing.T) { + imageBytes := []byte("value") + + result := PNGImage(imageBytes) + expectedDataURL := "data:image/png;base64," + imageBase64Value + + assert.Equal(t, expectedDataURL, result.Base64URL()) + assert.DeepEqual(t, imageBytes, result.Data()) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/mediavalue.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/mediavalue.go new file mode 100644 index 0000000..4b597b1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/mediavalue.go @@ -0,0 +1,65 @@ +package media + +import ( + "encoding/base64" + "fmt" +) + +// Media holds a piece of binary data. +type Media interface { + // Base64URL is the media encoded as a base64 URL. + Base64URL() string + + // MIME returns the media's MIME type. + MIME() string + + // Data returns the media's raw data. + Data() []byte +} + +// NewMedia will create a new appropriate media structure based on the MIME +// type provided. If no suitable structure exists, a Generic one will be used. +func NewMedia(mime string, data []byte) Media { + switch mime { + case ImageTypeJPEG: + return JPEGImage(data) + case ImageTypePNG: + return PNGImage(data) + default: + return NewGeneric(mime, data) + } +} + +// Generic holds binary data defined by its MIME type. +type Generic struct { + mime string + data []byte +} + +// NewGeneric creates a new Generic object. +func NewGeneric(mime string, data []byte) Generic { + return Generic{ + mime: mime, + data: data, + } +} + +// MIME returns the media's MIME type. +func (g Generic) MIME() string { + return g.mime +} + +// Base64URL is the media encoded as a base64 URL. +func (g Generic) Base64URL() string { + return base64URL(g.MIME(), g.data) +} + +// Data returns the media's raw data. +func (g Generic) Data() []byte { + return g.data +} + +func base64URL(mimeType string, data []byte) string { + base64EncodedImage := base64.StdEncoding.EncodeToString(data) + return fmt.Sprintf("data:%s;base64,%s", mimeType, base64EncodedImage) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/mediavalue_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/mediavalue_test.go new file mode 100644 index 0000000..609907a --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/media/mediavalue_test.go @@ -0,0 +1,35 @@ +package media + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestGeneric_Base64URL_create(t *testing.T) { + dataBytes := []byte("value") + mime := "foo/bar" + + result := NewGeneric(mime, dataBytes) + + expectedDataURL := "data:" + mime + ";base64," + imageBase64Value + + assert.Equal(t, expectedDataURL, result.Base64URL()) + assert.DeepEqual(t, dataBytes, result.Data()) +} + +func TestNewMedia(t *testing.T) { + dataBytes := []byte("value") + + v := NewMedia(ImageTypeJPEG, dataBytes) + _, ok := v.(JPEGImage) + assert.Assert(t, ok) + + v = NewMedia(ImageTypePNG, dataBytes) + _, ok = v.(PNGImage) + assert.Assert(t, ok) + + v = NewMedia("foo/bar", dataBytes) + _, ok = v.(Generic) + assert.Assert(t, ok) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/activity_details.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/activity_details.go new file mode 100644 index 0000000..71b3ebf --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/activity_details.go @@ -0,0 +1,48 @@ +package profile + +import ( + "time" + + "github.com/getyoti/yoti-go-sdk/v3/extra" +) + +// ActivityDetails represents the result of an activity between a user and the application. +type ActivityDetails struct { + UserProfile UserProfile + rememberMeID string + parentRememberMeID string + timestamp time.Time + receiptID string + ApplicationProfile ApplicationProfile + extraData *extra.Data +} + +// RememberMeID is a unique, stable identifier for a user in the context +// of an application. You can use it to identify returning users. +// This value will be different for the same user in different applications. +func (a ActivityDetails) RememberMeID() string { + return a.rememberMeID +} + +// ParentRememberMeID is a unique, stable identifier for a user in the +// context of an organisation. You can use it to identify returning users. +// This value is consistent for a given user across different applications +// belonging to a single organisation. +func (a ActivityDetails) ParentRememberMeID() string { + return a.parentRememberMeID +} + +// Timestamp is the Time and date of the sharing activity +func (a ActivityDetails) Timestamp() time.Time { + return a.timestamp +} + +// ReceiptID identifies a completed activity +func (a ActivityDetails) ReceiptID() string { + return a.receiptID +} + +// ExtraData represents extra pieces information on the receipt +func (a ActivityDetails) ExtraData() *extra.Data { + return a.extraData +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/address.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/address.go new file mode 100644 index 0000000..9d66fab --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/address.go @@ -0,0 +1,52 @@ +package profile + +import ( + "reflect" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +func getFormattedAddress(profile *UserProfile, formattedAddress string) *yotiprotoattr.Attribute { + proto := getProtobufAttribute(*profile, consts.AttrStructuredPostalAddress) + + return &yotiprotoattr.Attribute{ + Name: consts.AttrAddress, + Value: []byte(formattedAddress), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: proto.Anchors, + } +} + +func ensureAddressProfile(p *UserProfile) *attribute.StringAttribute { + if structuredPostalAddress, err := p.StructuredPostalAddress(); err == nil { + if (structuredPostalAddress != nil && !reflect.DeepEqual(structuredPostalAddress, attribute.JSONAttribute{})) { + var formattedAddress string + formattedAddress, err = retrieveFormattedAddressFromStructuredPostalAddress(structuredPostalAddress.Value()) + if err == nil && formattedAddress != "" { + return attribute.NewString(getFormattedAddress(p, formattedAddress)) + } + } + } + + return nil +} + +func retrieveFormattedAddressFromStructuredPostalAddress(structuredPostalAddress interface{}) (address string, err error) { + parsedStructuredAddressMap := structuredPostalAddress.(map[string]interface{}) + if formattedAddress, ok := parsedStructuredAddressMap["formatted_address"]; ok { + return formattedAddress.(string), nil + } + return +} + +func getProtobufAttribute(profile UserProfile, key string) *yotiprotoattr.Attribute { + for _, v := range profile.attributeSlice { + if v.Name == key { + return v + } + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/application_profile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/application_profile.go new file mode 100644 index 0000000..473a0ad --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/application_profile.go @@ -0,0 +1,50 @@ +package profile + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// Attribute names for application attributes +const ( + AttrConstApplicationName = "application_name" + AttrConstApplicationURL = "application_url" + AttrConstApplicationLogo = "application_logo" + AttrConstApplicationReceiptBGColor = "application_receipt_bgcolor" +) + +// ApplicationProfile is the profile of an application with convenience methods +// to access well-known attributes. +type ApplicationProfile struct { + baseProfile +} + +func newApplicationProfile(attributes *yotiprotoattr.AttributeList) ApplicationProfile { + return ApplicationProfile{ + baseProfile{ + attributeSlice: createAttributeSlice(attributes), + }, + } +} + +// ApplicationName is the name of the application +func (p ApplicationProfile) ApplicationName() *attribute.StringAttribute { + return p.GetStringAttribute(AttrConstApplicationName) +} + +// ApplicationURL is the URL where the application is available at +func (p ApplicationProfile) ApplicationURL() *attribute.StringAttribute { + return p.GetStringAttribute(AttrConstApplicationURL) +} + +// ApplicationReceiptBgColor is the background colour that will be displayed on +// each receipt the user gets as a result of a share with the application. +func (p ApplicationProfile) ApplicationReceiptBgColor() *attribute.StringAttribute { + return p.GetStringAttribute(AttrConstApplicationReceiptBGColor) +} + +// ApplicationLogo is the logo of the application that will be displayed to +// those users that perform a share with it. +func (p ApplicationProfile) ApplicationLogo() *attribute.ImageAttribute { + return p.GetImageAttribute(AttrConstApplicationLogo) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/age_verifications.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/age_verifications.go new file mode 100644 index 0000000..a7655d0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/age_verifications.go @@ -0,0 +1,34 @@ +package attribute + +import ( + "strconv" + "strings" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// AgeVerification encapsulates the result of a single age verification +// as part of a share +type AgeVerification struct { + Age int + CheckType string + Result bool + Attribute *yotiprotoattr.Attribute +} + +// NewAgeVerification constructs an AgeVerification from a protobuffer +func NewAgeVerification(attr *yotiprotoattr.Attribute) (verification AgeVerification, err error) { + split := strings.Split(attr.Name, ":") + verification.Age, err = strconv.Atoi(split[1]) + verification.CheckType = split[0] + + if string(attr.Value) == "true" { + verification.Result = true + } else { + verification.Result = false + } + + verification.Attribute = attr + + return +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/age_verifications_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/age_verifications_test.go new file mode 100644 index 0000000..b3a6e08 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/age_verifications_test.go @@ -0,0 +1,42 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestNewAgeVerification_ValueTrue(t *testing.T) { + attribute := &yotiprotoattr.Attribute{ + Name: "age_over:18", + Value: []byte("true"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + ageVerification, err := NewAgeVerification(attribute) + + assert.NilError(t, err) + + assert.Equal(t, ageVerification.Age, 18) + assert.Equal(t, ageVerification.CheckType, "age_over") + assert.Equal(t, ageVerification.Result, true) +} + +func TestNewAgeVerification_ValueFalse(t *testing.T) { + attribute := &yotiprotoattr.Attribute{ + Name: "age_under:30", + Value: []byte("false"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + ageVerification, err := NewAgeVerification(attribute) + + assert.NilError(t, err) + + assert.Equal(t, ageVerification.Age, 30) + assert.Equal(t, ageVerification.CheckType, "age_under") + assert.Equal(t, ageVerification.Result, false) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchor_parser.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchor_parser.go new file mode 100644 index 0000000..d1476c4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchor_parser.go @@ -0,0 +1,110 @@ +package anchor + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" + "google.golang.org/protobuf/proto" +) + +type anchorExtension struct { + Extension string `asn1:"tag:0,utf8"` +} + +var ( + sourceOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 47127, 1, 1, 1} + verifierOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 47127, 1, 1, 2} +) + +// ParseAnchors takes a slice of protobuf anchors, parses them, and returns a slice of Yoti SDK Anchors +func ParseAnchors(protoAnchors []*yotiprotoattr.Anchor) []*Anchor { + var processedAnchors []*Anchor + for _, protoAnchor := range protoAnchors { + parsedCerts := parseCertificates(protoAnchor.OriginServerCerts) + + anchorType, extension := getAnchorValuesFromCertificate(parsedCerts) + + parsedSignedTimestamp, err := parseSignedTimestamp(protoAnchor.SignedTimeStamp) + if err != nil { + continue + } + + processedAnchor := newAnchor(anchorType, parsedCerts, parsedSignedTimestamp, protoAnchor.SubType, extension) + + processedAnchors = append(processedAnchors, processedAnchor) + } + + return processedAnchors +} + +func getAnchorValuesFromCertificate(parsedCerts []*x509.Certificate) (anchorType Type, extension string) { + defaultAnchorType := TypeUnknown + + for _, cert := range parsedCerts { + for _, ext := range cert.Extensions { + var ( + value string + err error + ) + parsedAnchorType, value, err := parseExtension(ext) + if err != nil { + continue + } else if parsedAnchorType == TypeUnknown { + continue + } + return parsedAnchorType, value + } + } + + return defaultAnchorType, "" +} + +func parseExtension(ext pkix.Extension) (anchorType Type, val string, err error) { + anchorType = TypeUnknown + + switch { + case ext.Id.Equal(sourceOID): + anchorType = TypeSource + case ext.Id.Equal(verifierOID): + anchorType = TypeVerifier + default: + return anchorType, "", nil + } + + var ae anchorExtension + _, err = asn1.Unmarshal(ext.Value, &ae) + switch { + case err != nil: + return anchorType, "", fmt.Errorf("unable to unmarshal extension: %v", err) + case len(ae.Extension) == 0: + return anchorType, "", errors.New("empty extension") + default: + val = ae.Extension + } + + return anchorType, val, nil +} + +func parseSignedTimestamp(rawBytes []byte) (*yotiprotocom.SignedTimestamp, error) { + signedTimestamp := &yotiprotocom.SignedTimestamp{} + if err := proto.Unmarshal(rawBytes, signedTimestamp); err != nil { + return signedTimestamp, err + } + + return signedTimestamp, nil +} + +func parseCertificates(rawCerts [][]byte) (result []*x509.Certificate) { + for _, cert := range rawCerts { + parsedCertificate, _ := x509.ParseCertificate(cert) + + result = append(result, parsedCertificate) + } + + return result +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchor_parser_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchor_parser_test.go new file mode 100644 index 0000000..13849a3 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchor_parser_test.go @@ -0,0 +1,147 @@ +package anchor + +import ( + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" +) + +func assertServerCertSerialNo(t *testing.T, expectedSerialNo string, actualSerialNo *big.Int) { + expectedSerialNoBigInt := new(big.Int) + expectedSerialNoBigInt, ok := expectedSerialNoBigInt.SetString(expectedSerialNo, 10) + assert.Assert(t, ok, "Unexpected error when setting string as big int") + + assert.Equal(t, expectedSerialNoBigInt.Cmp(actualSerialNo), 0) // 0 == equivalent +} + +func createAnchorSliceFromTestFile(t *testing.T, filename string) []*yotiprotoattr.Anchor { + anchorBytes := test.DecodeTestFile(t, filename) + + protoAnchor := &yotiprotoattr.Anchor{} + err2 := proto.Unmarshal(anchorBytes, protoAnchor) + assert.NilError(t, err2) + + protoAnchors := append([]*yotiprotoattr.Anchor{}, protoAnchor) + + return protoAnchors +} + +func TestAnchorParser_parseExtension_ShouldErrorForInvalidExtension(t *testing.T) { + invalidExt := pkix.Extension{ + Id: sourceOID, + } + + _, _, err := parseExtension(invalidExt) + + assert.Check(t, err != nil) + assert.Error(t, err, "unable to unmarshal extension: asn1: syntax error: sequence truncated") +} + +func TestAnchorParser_Passport(t *testing.T) { + anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_passport.txt") + + parsedAnchors := ParseAnchors(anchorSlice) + + actualAnchor := parsedAnchors[0] + + assert.Equal(t, actualAnchor.Type(), TypeSource) + + expectedDate := time.Date(2018, time.April, 12, 13, 14, 32, 835537e3, time.UTC) + actualDate := actualAnchor.SignedTimestamp().Timestamp().UTC() + assert.Equal(t, actualDate, expectedDate) + + expectedSubType := "OCR" + assert.Equal(t, actualAnchor.SubType(), expectedSubType) + + expectedValue := "PASSPORT" + assert.Equal(t, actualAnchor.Value(), expectedValue) + + actualSerialNo := actualAnchor.OriginServerCerts()[0].SerialNumber + assertServerCertSerialNo(t, "277870515583559162487099305254898397834", actualSerialNo) +} + +func TestAnchorParser_DrivingLicense(t *testing.T) { + anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_driving_license.txt") + + parsedAnchors := ParseAnchors(anchorSlice) + resultAnchor := parsedAnchors[0] + + assert.Equal(t, resultAnchor.Type(), TypeSource) + + expectedDate := time.Date(2018, time.April, 11, 12, 13, 3, 923537e3, time.UTC) + actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC() + assert.Equal(t, actualDate, expectedDate) + + expectedSubType := "" + assert.Equal(t, resultAnchor.SubType(), expectedSubType) + + expectedValue := "DRIVING_LICENCE" + assert.Equal(t, resultAnchor.Value(), expectedValue) + + actualSerialNo := resultAnchor.OriginServerCerts()[0].SerialNumber + assertServerCertSerialNo(t, "46131813624213904216516051554755262812", actualSerialNo) +} + +func TestAnchorParser_UnknownAnchor(t *testing.T) { + anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_unknown.txt") + + resultAnchor := ParseAnchors(anchorSlice)[0] + + expectedDate := time.Date(2019, time.March, 5, 10, 45, 11, 840037e3, time.UTC) + actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC() + assert.Equal(t, actualDate, expectedDate) + + expectedSubType := "TEST UNKNOWN SUB TYPE" + expectedType := TypeUnknown + assert.Equal(t, resultAnchor.SubType(), expectedSubType) + assert.Equal(t, resultAnchor.Type(), expectedType) + assert.Equal(t, resultAnchor.Value(), "") +} + +func TestAnchorParser_YotiAdmin(t *testing.T) { + anchorSlice := createAnchorSliceFromTestFile(t, "../../../test/fixtures/test_anchor_yoti_admin.txt") + + resultAnchor := ParseAnchors(anchorSlice)[0] + + assert.Equal(t, resultAnchor.Type(), TypeVerifier) + + expectedDate := time.Date(2018, time.April, 11, 12, 13, 4, 95238e3, time.UTC) + actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC() + assert.Equal(t, actualDate, expectedDate) + + expectedSubType := "" + assert.Equal(t, resultAnchor.SubType(), expectedSubType) + + expectedValue := "YOTI_ADMIN" + assert.Equal(t, resultAnchor.Value(), expectedValue) + + actualSerialNo := resultAnchor.OriginServerCerts()[0].SerialNumber + assertServerCertSerialNo(t, "256616937783084706710155170893983549581", actualSerialNo) +} + +func TestAnchors_None(t *testing.T) { + var anchorSlice []*Anchor + + sources := GetSources(anchorSlice) + assert.Equal(t, len(sources), 0, "GetSources should not return anything with empty anchors") + + verifiers := GetVerifiers(anchorSlice) + assert.Equal(t, len(verifiers), 0, "GetVerifiers should not return anything with empty anchors") +} + +func TestAnchorParser_InvalidSignedTimestamp(t *testing.T) { + var protoAnchors []*yotiprotoattr.Anchor + protoAnchors = append(protoAnchors, &yotiprotoattr.Anchor{ + SignedTimeStamp: []byte("invalidProto"), + }) + parsedAnchors := ParseAnchors(protoAnchors) + + var expectedAnchors []*Anchor + assert.DeepEqual(t, expectedAnchors, parsedAnchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchors.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchors.go new file mode 100644 index 0000000..839a6e1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchors.go @@ -0,0 +1,105 @@ +package anchor + +import ( + "crypto/x509" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" +) + +// Anchor is the metadata associated with an attribute. It describes how an attribute has been provided +// to Yoti (SOURCE Anchor) and how it has been verified (VERIFIER Anchor). +// If an attribute has only one SOURCE Anchor with the value set to +// "USER_PROVIDED" and zero VERIFIER Anchors, then the attribute +// is a self-certified one. +type Anchor struct { + anchorType Type + originServerCerts []*x509.Certificate + signedTimestamp SignedTimestamp + subtype string + value string +} + +func newAnchor(anchorType Type, originServerCerts []*x509.Certificate, signedTimestamp *yotiprotocom.SignedTimestamp, subtype string, value string) *Anchor { + return &Anchor{ + anchorType: anchorType, + originServerCerts: originServerCerts, + signedTimestamp: convertSignedTimestamp(signedTimestamp), + subtype: subtype, + value: value, + } +} + +// Type Anchor type, based on the Object Identifier (OID) +type Type int + +const ( + // TypeUnknown - default value + TypeUnknown Type = 1 + iota + // TypeSource - how the anchor has been sourced + TypeSource + // TypeVerifier - how the anchor has been verified + TypeVerifier +) + +// Type of the Anchor - most likely either SOURCE or VERIFIER, but it's +// possible that new Anchor types will be added in future. +func (a Anchor) Type() Type { + return a.anchorType +} + +// OriginServerCerts are the X.509 certificate chain(DER-encoded ASN.1) +// from the service that assigned the attribute. +// +// The first certificate in the chain holds the public key that can be +// used to verify the Signature field; any following entries (zero or +// more) are for intermediate certificate authorities (in order). +// +// The last certificate in the chain must be verified against the Yoti root +// CA certificate. An extension in the first certificate holds the main artifact type, +// e.g. “PASSPORT”, which can be retrieved with .Value(). +func (a Anchor) OriginServerCerts() []*x509.Certificate { + return a.originServerCerts +} + +// SignedTimestamp is the time at which the signature was created. The +// message associated with the timestamp is the marshaled form of +// AttributeSigning (i.e. the same message that is signed in the +// Signature field). This method returns the SignedTimestamp +// object, the actual timestamp as a *time.Time can be called with +// .Timestamp() on the result of this function. +func (a Anchor) SignedTimestamp() SignedTimestamp { + return a.signedTimestamp +} + +// SubType is an indicator of any specific processing method, or +// subcategory, pertaining to an artifact. For example, for a passport, this would be +// either "NFC" or "OCR". +func (a Anchor) SubType() string { + return a.subtype +} + +// Value identifies the provider that either sourced or verified the attribute value. +// The range of possible values is not limited. For a SOURCE anchor, expect a value like +// PASSPORT, DRIVING_LICENSE. For a VERIFIER anchor, expect a value like YOTI_ADMIN. +func (a Anchor) Value() string { + return a.value +} + +// GetSources returns the anchors which identify how and when an attribute value was acquired. +func GetSources(anchors []*Anchor) (sources []*Anchor) { + return filterAnchors(anchors, TypeSource) +} + +// GetVerifiers returns the anchors which identify how and when an attribute value was verified by another provider. +func GetVerifiers(anchors []*Anchor) (sources []*Anchor) { + return filterAnchors(anchors, TypeVerifier) +} + +func filterAnchors(anchors []*Anchor, anchorType Type) (result []*Anchor) { + for _, v := range anchors { + if v.anchorType == anchorType { + result = append(result, v) + } + } + return result +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchors_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchors_test.go new file mode 100644 index 0000000..ed5287e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/anchors_test.go @@ -0,0 +1,20 @@ +package anchor + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestFilterAnchors_FilterSources(t *testing.T) { + anchorSlice := []*Anchor{ + {subtype: "a", anchorType: TypeSource}, + {subtype: "b", anchorType: TypeVerifier}, + {subtype: "c", anchorType: TypeSource}, + } + sources := filterAnchors(anchorSlice, TypeSource) + assert.Equal(t, len(sources), 2) + assert.Equal(t, sources[0].subtype, "a") + assert.Equal(t, sources[1].subtype, "c") + +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/signed_timestamp.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/signed_timestamp.go new file mode 100644 index 0000000..2081b7d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/anchor/signed_timestamp.go @@ -0,0 +1,35 @@ +package anchor + +import ( + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" +) + +// SignedTimestamp is the object which contains a timestamp +type SignedTimestamp struct { + version int32 + timestamp *time.Time +} + +func convertSignedTimestamp(protoSignedTimestamp *yotiprotocom.SignedTimestamp) SignedTimestamp { + uintTimestamp := protoSignedTimestamp.Timestamp + intTimestamp := int64(uintTimestamp) + unixTime := time.Unix(intTimestamp/1e6, (intTimestamp%1e6)*1e3) + + return SignedTimestamp{ + version: protoSignedTimestamp.Version, + timestamp: &unixTime, + } +} + +// Version indicates both the version of the protobuf message in use, +// as well as the specific hash algorithms. +func (s SignedTimestamp) Version() int32 { + return s.version +} + +// Timestamp is a point in time, to the nearest microsecond. +func (s SignedTimestamp) Timestamp() *time.Time { + return s.timestamp +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/attribute_details.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/attribute_details.go new file mode 100644 index 0000000..a380150 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/attribute_details.go @@ -0,0 +1,48 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" +) + +// attributeDetails is embedded in each attribute for fields common to all +// attributes +type attributeDetails struct { + name string + contentType string + anchors []*anchor.Anchor + id *string +} + +// Name gets the attribute name +func (a attributeDetails) Name() string { + return a.name +} + +// ID gets the attribute ID +func (a attributeDetails) ID() *string { + return a.id +} + +// ContentType gets the attribute's content type description +func (a attributeDetails) ContentType() string { + return a.contentType +} + +// Anchors are the metadata associated with an attribute. They describe +// how an attribute has been provided to Yoti (SOURCE Anchor) and how +// it has been verified (VERIFIER Anchor). +func (a attributeDetails) Anchors() []*anchor.Anchor { + return a.anchors +} + +// Sources returns the anchors which identify how and when an attribute value +// was acquired. +func (a attributeDetails) Sources() []*anchor.Anchor { + return anchor.GetSources(a.anchors) +} + +// Verifiers returns the anchors which identify how and when an attribute value +// was verified by another provider. +func (a attributeDetails) Verifiers() []*anchor.Anchor { + return anchor.GetVerifiers(a.anchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/attribute_test.go new file mode 100644 index 0000000..67b6c2b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/attribute_test.go @@ -0,0 +1,36 @@ +package attribute + +import ( + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestNewThirdPartyAttribute(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_third_party.txt") + + stringAttribute := NewString(protoAttribute) + + assert.Equal(t, stringAttribute.Value(), "test-third-party-attribute-0") + assert.Equal(t, stringAttribute.Name(), "com.thirdparty.id") + + assert.Equal(t, stringAttribute.Sources()[0].Value(), "THIRD_PARTY") + assert.Equal(t, stringAttribute.Sources()[0].SubType(), "orgName") + + assert.Equal(t, stringAttribute.Verifiers()[0].Value(), "THIRD_PARTY") + assert.Equal(t, stringAttribute.Verifiers()[0].SubType(), "orgName") +} + +func TestAttribute_DateOfBirth(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_date_of_birth.txt") + + dateOfBirthAttribute, err := NewDate(protoAttribute) + + assert.NilError(t, err) + + expectedDateOfBirth := time.Date(1970, time.December, 01, 0, 0, 0, 0, time.UTC) + actualDateOfBirth := dateOfBirthAttribute.Value() + + assert.Assert(t, actualDateOfBirth.Equal(expectedDateOfBirth)) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/date_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/date_attribute.go new file mode 100644 index 0000000..cdc55ce --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/date_attribute.go @@ -0,0 +1,39 @@ +package attribute + +import ( + "time" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// DateAttribute is a Yoti attribute which returns a date as *time.Time for its value +type DateAttribute struct { + attributeDetails + value *time.Time +} + +// NewDate creates a new Date attribute +func NewDate(a *yotiprotoattr.Attribute) (*DateAttribute, error) { + parsedTime, err := time.Parse("2006-01-02", string(a.Value)) + if err != nil { + return nil, err + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &DateAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: &parsedTime, + }, nil +} + +// Value returns the value of the TimeAttribute as *time.Time +func (a *DateAttribute) Value() *time.Time { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/date_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/date_attribute_test.go new file mode 100644 index 0000000..24807c9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/date_attribute_test.go @@ -0,0 +1,44 @@ +package attribute + +import ( + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestTimeAttribute_NewDate_DateOnly(t *testing.T) { + proto := yotiprotoattr.Attribute{ + Value: []byte("2011-12-25"), + } + + timeAttribute, err := NewDate(&proto) + assert.NilError(t, err) + + assert.Equal(t, *timeAttribute.Value(), time.Date(2011, 12, 25, 0, 0, 0, 0, time.UTC)) +} + +func TestTimeAttribute_DateOfBirth(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_date_of_birth.txt") + + dateOfBirthAttribute, err := NewDate(protoAttribute) + + assert.NilError(t, err) + + expectedDateOfBirth := time.Date(1970, time.December, 01, 0, 0, 0, 0, time.UTC) + actualDateOfBirth := dateOfBirthAttribute.Value() + + assert.Assert(t, actualDateOfBirth.Equal(expectedDateOfBirth)) +} + +func TestNewTime_ShouldReturnErrorForInvalidDate(t *testing.T) { + proto := yotiprotoattr.Attribute{ + Name: "example", + Value: []byte("2006-60-20"), + ContentType: yotiprotoattr.ContentType_DATE, + } + attribute, err := NewDate(&proto) + assert.Check(t, attribute == nil) + assert.ErrorContains(t, err, "month out of range") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/definition.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/definition.go new file mode 100644 index 0000000..b0d4b8a --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/definition.go @@ -0,0 +1,31 @@ +package attribute + +import ( + "encoding/json" +) + +// Definition contains information about the attribute(s) issued by a third party. +type Definition struct { + name string +} + +// Name of the attribute to be issued. +func (a Definition) Name() string { + return a.name +} + +// MarshalJSON returns encoded json +func (a Definition) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Name string `json:"name"` + }{ + Name: a.name, + }) +} + +// NewAttributeDefinition returns a new AttributeDefinition +func NewAttributeDefinition(s string) Definition { + return Definition{ + name: s, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/definition_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/definition_test.go new file mode 100644 index 0000000..b209e02 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/definition_test.go @@ -0,0 +1,18 @@ +package attribute + +import ( + "encoding/json" + "fmt" +) + +func ExampleDefinition_MarshalJSON() { + exampleDefinition := NewAttributeDefinition("exampleDefinition") + marshalledJSON, err := json.Marshal(exampleDefinition) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) + // Output: {"name":"exampleDefinition"} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/document_details_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/document_details_attribute.go new file mode 100644 index 0000000..a18ccab --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/document_details_attribute.go @@ -0,0 +1,87 @@ +package attribute + +import ( + "fmt" + "strings" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +const ( + documentDetailsDateFormatConst = "2006-01-02" +) + +// DocumentDetails represents information extracted from a document provided by the user +type DocumentDetails struct { + DocumentType string + IssuingCountry string + DocumentNumber string + ExpirationDate *time.Time + IssuingAuthority string +} + +// DocumentDetailsAttribute wraps a document details with anchor data +type DocumentDetailsAttribute struct { + attributeDetails + value DocumentDetails +} + +// Value returns the document details struct attached to this attribute +func (attr *DocumentDetailsAttribute) Value() DocumentDetails { + return attr.value +} + +// NewDocumentDetails creates a DocumentDetailsAttribute which wraps a +// DocumentDetails with anchor data +func NewDocumentDetails(a *yotiprotoattr.Attribute) (*DocumentDetailsAttribute, error) { + parsedAnchors := anchor.ParseAnchors(a.Anchors) + details := DocumentDetails{} + err := details.Parse(string(a.Value)) + if err != nil { + return nil, err + } + + return &DocumentDetailsAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: details, + }, nil +} + +// Parse fills a DocumentDetails object from a raw string +func (details *DocumentDetails) Parse(data string) error { + dataSlice := strings.Split(data, " ") + + if len(dataSlice) < 3 { + return fmt.Errorf("Document Details data is invalid, %s", data) + } + for _, section := range dataSlice { + if section == "" { + return fmt.Errorf("Document Details data is invalid %s", data) + } + } + + details.DocumentType = dataSlice[0] + details.IssuingCountry = dataSlice[1] + details.DocumentNumber = dataSlice[2] + if len(dataSlice) > 3 && dataSlice[3] != "-" { + expirationDateData, dateErr := time.Parse(documentDetailsDateFormatConst, dataSlice[3]) + + if dateErr == nil { + details.ExpirationDate = &expirationDateData + } else { + return dateErr + } + } + if len(dataSlice) > 4 { + details.IssuingAuthority = dataSlice[4] + } + + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/document_details_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/document_details_attribute_test.go new file mode 100644 index 0000000..bf2e7de --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/document_details_attribute_test.go @@ -0,0 +1,185 @@ +package attribute + +import ( + "fmt" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func ExampleDocumentDetails_Parse() { + raw := "PASSPORT GBR 1234567 2022-09-12" + details := DocumentDetails{} + err := details.Parse(raw) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Printf( + "Document Type: %s, Issuing Country: %s, Document Number: %s, Expiration Date: %s", + details.DocumentType, + details.IssuingCountry, + details.DocumentNumber, + details.ExpirationDate, + ) + // Output: Document Type: PASSPORT, Issuing Country: GBR, Document Number: 1234567, Expiration Date: 2022-09-12 00:00:00 +0000 UTC +} + +func ExampleNewDocumentDetails() { + proto := yotiprotoattr.Attribute{ + Name: "exampleDocumentDetails", + Value: []byte("PASSPORT GBR 1234567 2022-09-12"), + ContentType: yotiprotoattr.ContentType_STRING, + } + attribute, err := NewDocumentDetails(&proto) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Printf( + "Document Type: %s, With %d Anchors", + attribute.Value().DocumentType, + len(attribute.Anchors()), + ) + // Output: Document Type: PASSPORT, With 0 Anchors +} + +func TestDocumentDetailsShouldParseDrivingLicenceWithoutExpiry(t *testing.T) { + drivingLicenceGBR := "PASS_CARD GBR 1234abc - DVLA" + + details := DocumentDetails{} + err := details.Parse(drivingLicenceGBR) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "PASS_CARD") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Assert(t, details.ExpirationDate == nil) + assert.Equal(t, details.IssuingCountry, "GBR") + assert.Equal(t, details.IssuingAuthority, "DVLA") +} + +func TestDocumentDetailsShouldParseRedactedAadhar(t *testing.T) { + aadhaar := "AADHAAR IND ****1234 2016-05-01" + details := DocumentDetails{} + err := details.Parse(aadhaar) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "AADHAAR") + assert.Equal(t, details.DocumentNumber, "****1234") + assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01") + assert.Equal(t, details.IssuingCountry, "IND") + assert.Equal(t, details.IssuingAuthority, "") +} + +func TestDocumentDetailsShouldParseSpecialCharacters(t *testing.T) { + testData := [][]string{ + {"type country **** - authority", "****"}, + {"type country ~!@#$%^&*()-_=+[]{}|;':,./<>? - authority", "~!@#$%^&*()-_=+[]{}|;':,./<>?"}, + {"type country \"\" - authority", "\"\""}, + {"type country \\ - authority", "\\"}, + {"type country \" - authority", "\""}, + {"type country '' - authority", "''"}, + {"type country ' - authority", "'"}, + } + for _, row := range testData { + details := DocumentDetails{} + err := details.Parse(row[0]) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentNumber, row[1]) + } +} + +func TestDocumentDetailsShouldFailOnDoubleSpace(t *testing.T) { + data := "AADHAAR IND ****1234" + details := DocumentDetails{} + err := details.Parse(data) + assert.Check(t, err != nil) + assert.ErrorContains(t, err, "Document Details data is invalid") +} + +func TestDocumentDetailsShouldParseDrivingLicenceWithExtraAttribute(t *testing.T) { + drivingLicenceGBR := "DRIVING_LICENCE GBR 1234abc 2016-05-01 DVLA someThirdAttribute" + details := DocumentDetails{} + err := details.Parse(drivingLicenceGBR) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "DRIVING_LICENCE") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01") + assert.Equal(t, details.IssuingCountry, "GBR") + assert.Equal(t, details.IssuingAuthority, "DVLA") +} + +func TestDocumentDetailsShouldParseDrivingLicenceWithAllOptionalAttributes(t *testing.T) { + drivingLicenceGBR := "DRIVING_LICENCE GBR 1234abc 2016-05-01 DVLA" + + details := DocumentDetails{} + err := details.Parse(drivingLicenceGBR) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "DRIVING_LICENCE") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01") + assert.Equal(t, details.IssuingCountry, "GBR") + assert.Equal(t, details.IssuingAuthority, "DVLA") +} + +func TestDocumentDetailsShouldParseAadhaar(t *testing.T) { + aadhaar := "AADHAAR IND 1234abc 2016-05-01" + + details := DocumentDetails{} + err := details.Parse(aadhaar) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "AADHAAR") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Equal(t, details.ExpirationDate.Format("2006-01-02"), "2016-05-01") + assert.Equal(t, details.IssuingCountry, "IND") +} + +func TestDocumentDetailsShouldParsePassportWithMandatoryFieldsOnly(t *testing.T) { + passportGBR := "PASSPORT GBR 1234abc" + + details := DocumentDetails{} + err := details.Parse(passportGBR) + if err != nil { + t.Fail() + } + assert.Equal(t, details.DocumentType, "PASSPORT") + assert.Equal(t, details.DocumentNumber, "1234abc") + assert.Assert(t, details.ExpirationDate == nil) + assert.Equal(t, details.IssuingCountry, "GBR") + assert.Equal(t, details.IssuingAuthority, "") +} + +func TestDocumentDetailsShouldErrorOnEmptyString(t *testing.T) { + empty := "" + + details := DocumentDetails{} + err := details.Parse(empty) + assert.ErrorContains(t, err, "Document Details data is invalid") +} + +func TestDocumentDetailsShouldErrorIfLessThan3Words(t *testing.T) { + corrupt := "PASS_CARD GBR" + details := DocumentDetails{} + err := details.Parse(corrupt) + assert.ErrorContains(t, err, "Document Details data is invalid") +} + +func TestDocumentDetailsShouldErrorForInvalidExpirationDate(t *testing.T) { + corrupt := "PASSPORT GBR 1234abc X016-05-01" + details := DocumentDetails{} + err := details.Parse(corrupt) + assert.ErrorContains(t, err, "cannot parse") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/generic_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/generic_attribute.go new file mode 100644 index 0000000..c729e30 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/generic_attribute.go @@ -0,0 +1,38 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// GenericAttribute is a Yoti attribute which returns a generic value +type GenericAttribute struct { + attributeDetails + value interface{} +} + +// NewGeneric creates a new generic attribute +func NewGeneric(a *yotiprotoattr.Attribute) *GenericAttribute { + value, err := parseValue(a.ContentType, a.Value) + + if err != nil { + return nil + } + + var parsedAnchors = anchor.ParseAnchors(a.Anchors) + + return &GenericAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: value, + } +} + +// Value returns the value of the GenericAttribute as an interface +func (a *GenericAttribute) Value() interface{} { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/generic_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/generic_attribute_test.go new file mode 100644 index 0000000..e2daae8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/generic_attribute_test.go @@ -0,0 +1,39 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestNewGeneric_ShouldParseUnknownTypeAsString(t *testing.T) { + value := []byte("value") + protoAttr := yotiprotoattr.Attribute{ + ContentType: yotiprotoattr.ContentType_UNDEFINED, + Value: value, + } + parsed := NewGeneric(&protoAttr) + + stringValue, ok := parsed.Value().(string) + assert.Check(t, ok) + + assert.Equal(t, stringValue, string(value)) +} + +func TestGeneric_ContentType(t *testing.T) { + attribute := GenericAttribute{ + attributeDetails: attributeDetails{ + contentType: "contentType", + }, + } + + assert.Equal(t, attribute.ContentType(), "contentType") +} + +func TestNewGeneric_ShouldReturnNilForInvalidProtobuf(t *testing.T) { + invalid := NewGeneric(&yotiprotoattr.Attribute{ + ContentType: yotiprotoattr.ContentType_JSON, + }) + assert.Check(t, invalid == nil) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/helper_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/helper_test.go new file mode 100644 index 0000000..47c28ea --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/helper_test.go @@ -0,0 +1,21 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" +) + +func createAttributeFromTestFile(t *testing.T, filename string) *yotiprotoattr.Attribute { + attributeBytes := test.DecodeTestFile(t, filename) + + attributeStruct := &yotiprotoattr.Attribute{} + + err2 := proto.Unmarshal(attributeBytes, attributeStruct) + assert.NilError(t, err2) + + return attributeStruct +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_attribute.go new file mode 100644 index 0000000..fd9d7f1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_attribute.go @@ -0,0 +1,53 @@ +package attribute + +import ( + "errors" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// ImageAttribute is a Yoti attribute which returns an image as its value +type ImageAttribute struct { + attributeDetails + value media.Media +} + +// NewImage creates a new Image attribute +func NewImage(a *yotiprotoattr.Attribute) (*ImageAttribute, error) { + imageValue, err := parseImageValue(a.ContentType, a.Value) + if err != nil { + return nil, err + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &ImageAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: imageValue, + }, nil +} + +// Value returns the value of the ImageAttribute as media.Media +func (a *ImageAttribute) Value() media.Media { + return a.value +} + +func parseImageValue(contentType yotiprotoattr.ContentType, byteValue []byte) (media.Media, error) { + switch contentType { + case yotiprotoattr.ContentType_JPEG: + return media.JPEGImage(byteValue), nil + + case yotiprotoattr.ContentType_PNG: + return media.PNGImage(byteValue), nil + + default: + return nil, errors.New("cannot create Image with unsupported type") + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_attribute_test.go new file mode 100644 index 0000000..2fe620f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_attribute_test.go @@ -0,0 +1,106 @@ +package attribute + +import ( + "encoding/base64" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestImageAttribute_Image_Png(t *testing.T) { + attributeName := consts.AttrSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + assert.DeepEqual(t, selfie.Value().Data(), byteValue) +} + +func TestImageAttribute_Image_Jpeg(t *testing.T) { + attributeName := consts.AttrSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + assert.DeepEqual(t, selfie.Value().Data(), byteValue) +} + +func TestImageAttribute_Image_Default(t *testing.T) { + attributeName := consts.AttrSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + assert.DeepEqual(t, selfie.Value().Data(), byteValue) +} + +func TestImageAttribute_Base64Selfie_Png(t *testing.T) { + attributeName := consts.AttrSelfie + imageBytes := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: imageBytes, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(imageBytes) + + expectedBase64Selfie := "data:image/png;base64," + base64ImageExpectedValue + + base64Selfie := selfie.Value().Base64URL() + + assert.Equal(t, base64Selfie, expectedBase64Selfie) +} + +func TestImageAttribute_Base64URL_Jpeg(t *testing.T) { + attributeName := consts.AttrSelfie + imageBytes := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: imageBytes, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + selfie, err := NewImage(attributeImage) + assert.NilError(t, err) + + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(imageBytes) + + expectedBase64Selfie := "data:image/jpeg;base64," + base64ImageExpectedValue + + base64Selfie := selfie.Value().Base64URL() + + assert.Equal(t, base64Selfie, expectedBase64Selfie) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_slice_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_slice_attribute.go new file mode 100644 index 0000000..de507ab --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_slice_attribute.go @@ -0,0 +1,69 @@ +package attribute + +import ( + "errors" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// ImageSliceAttribute is a Yoti attribute which returns a slice of images as its value +type ImageSliceAttribute struct { + attributeDetails + value []media.Media +} + +// NewImageSlice creates a new ImageSlice attribute +func NewImageSlice(a *yotiprotoattr.Attribute) (*ImageSliceAttribute, error) { + if a.ContentType != yotiprotoattr.ContentType_MULTI_VALUE { + return nil, errors.New("creating an Image Slice attribute with content types other than MULTI_VALUE is not supported") + } + + parsedMultiValue, err := parseMultiValue(a.Value) + + if err != nil { + return nil, err + } + + var imageSliceValue []media.Media + if parsedMultiValue != nil { + imageSliceValue, err = CreateImageSlice(parsedMultiValue) + if err != nil { + return nil, err + } + } + + return &ImageSliceAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: anchor.ParseAnchors(a.Anchors), + id: &a.EphemeralId, + }, + value: imageSliceValue, + }, nil +} + +// CreateImageSlice takes a slice of Items, and converts them into a slice of images +func CreateImageSlice(items []*Item) (result []media.Media, err error) { + for _, item := range items { + + switch i := item.Value.(type) { + case media.PNGImage: + result = append(result, i) + case media.JPEGImage: + result = append(result, i) + default: + return nil, fmt.Errorf("unexpected item type %T", i) + } + } + + return result, nil +} + +// Value returns the value of the ImageSliceAttribute +func (a *ImageSliceAttribute) Value() []media.Media { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_slice_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_slice_attribute_test.go new file mode 100644 index 0000000..2c30092 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/image_slice_attribute_test.go @@ -0,0 +1,61 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func assertIsExpectedImage(t *testing.T, image media.Media, imageMIMEType string, expectedBase64URLLast10 string) { + assert.Equal(t, image.MIME(), imageMIMEType) + + actualBase64URL := image.Base64URL() + + ActualBase64URLLast10Chars := actualBase64URL[len(actualBase64URL)-10:] + + assert.Equal(t, ActualBase64URLLast10Chars, expectedBase64URLLast10) +} + +func assertIsExpectedDocumentImagesAttribute(t *testing.T, actualDocumentImages []media.Media, anchor *anchor.Anchor) { + + assert.Equal(t, len(actualDocumentImages), 2, "This Document Images attribute should have two images") + + assertIsExpectedImage(t, actualDocumentImages[0], media.ImageTypeJPEG, "vWgD//2Q==") + assertIsExpectedImage(t, actualDocumentImages[1], media.ImageTypeJPEG, "38TVEH/9k=") + + expectedValue := "NATIONAL_ID" + assert.Equal(t, anchor.Value(), expectedValue) + + expectedSubType := "STATE_ID" + assert.Equal(t, anchor.SubType(), expectedSubType) +} + +func TestAttribute_NewImageSlice(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt") + + documentImagesAttribute, err := NewImageSlice(protoAttribute) + + assert.NilError(t, err) + + assertIsExpectedDocumentImagesAttribute(t, documentImagesAttribute.Value(), documentImagesAttribute.Anchors()[0]) +} + +func TestAttribute_ImageSliceNotCreatedWithNonMultiValueType(t *testing.T) { + attributeName := "attributeName" + attributeValueString := "value" + attributeValue := []byte(attributeValueString) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + _, err := NewImageSlice(attr) + + assert.Assert(t, err != nil, "Expected error when creating image slice from attribute which isn't of multi-value type") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/issuance_details.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/issuance_details.go new file mode 100644 index 0000000..381d4e9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/issuance_details.go @@ -0,0 +1,86 @@ +package attribute + +import ( + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare" + "google.golang.org/protobuf/proto" +) + +// IssuanceDetails contains information about the attribute(s) issued by a third party +type IssuanceDetails struct { + token string + expiryDate *time.Time + attributes []Definition +} + +// Token is the issuance token that can be used to retrieve the user's stored details. +// These details will be used to issue attributes on behalf of an organisation to that user. +func (i IssuanceDetails) Token() string { + return i.token +} + +// ExpiryDate is the timestamp at which the request for the attribute value +// from third party will expire. Will be nil if not provided. +func (i IssuanceDetails) ExpiryDate() *time.Time { + return i.expiryDate +} + +// Attributes information about the attributes the third party would like to issue. +func (i IssuanceDetails) Attributes() []Definition { + return i.attributes +} + +// ParseIssuanceDetails takes the Third Party Attribute object and converts it into an IssuanceDetails struct +func ParseIssuanceDetails(thirdPartyAttributeBytes []byte) (*IssuanceDetails, error) { + thirdPartyAttributeStruct := &yotiprotoshare.ThirdPartyAttribute{} + if err := proto.Unmarshal(thirdPartyAttributeBytes, thirdPartyAttributeStruct); err != nil { + return nil, fmt.Errorf("unable to parse ThirdPartyAttribute value: %q. Error: %q", string(thirdPartyAttributeBytes), err) + } + + var issuingAttributesProto = thirdPartyAttributeStruct.GetIssuingAttributes() + var issuingAttributeDefinitions = parseIssuingAttributeDefinitions(issuingAttributesProto.GetDefinitions()) + + expiryDate, dateParseErr := parseExpiryDate(issuingAttributesProto.ExpiryDate) + + var issuanceTokenBytes = thirdPartyAttributeStruct.GetIssuanceToken() + + if len(issuanceTokenBytes) == 0 { + return nil, errors.New("Issuance Token is invalid") + } + + base64EncodedToken := base64.StdEncoding.EncodeToString(issuanceTokenBytes) + + return &IssuanceDetails{ + token: base64EncodedToken, + expiryDate: expiryDate, + attributes: issuingAttributeDefinitions, + }, dateParseErr +} + +func parseIssuingAttributeDefinitions(definitions []*yotiprotoshare.Definition) (issuingAttributes []Definition) { + for _, definition := range definitions { + attributeDefinition := Definition{ + name: definition.Name, + } + issuingAttributes = append(issuingAttributes, attributeDefinition) + } + + return issuingAttributes +} + +func parseExpiryDate(expiryDateString string) (*time.Time, error) { + if expiryDateString == "" { + return nil, nil + } + + parsedTime, err := time.Parse(time.RFC3339Nano, expiryDateString) + if err != nil { + return nil, err + } + + return &parsedTime, err +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/issuance_details_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/issuance_details_test.go new file mode 100644 index 0000000..462d863 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/issuance_details_test.go @@ -0,0 +1,145 @@ +package attribute + +import ( + "encoding/base64" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/test" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare" + "google.golang.org/protobuf/proto" + + "gotest.tools/v3/assert" + + is "gotest.tools/v3/assert/cmp" +) + +func TestShouldParseThirdPartyAttributeCorrectly(t *testing.T) { + var thirdPartyAttributeBytes = test.GetTestFileBytes(t, "../../test/fixtures/test_third_party_issuance_details.txt") + issuanceDetails, err := ParseIssuanceDetails(thirdPartyAttributeBytes) + + assert.NilError(t, err) + assert.Equal(t, issuanceDetails.Attributes()[0].Name(), "com.thirdparty.id") + assert.Equal(t, issuanceDetails.Token(), "c29tZUlzc3VhbmNlVG9rZW4=") + assert.Equal(t, + issuanceDetails.ExpiryDate().Format("2006-01-02T15:04:05.000Z"), + "2019-10-15T22:04:05.123Z") +} + +func TestShouldLogWarningIfErrorInParsingExpiryDate(t *testing.T) { + var tokenValue = "41548a175dfaw" + thirdPartyAttribute := &yotiprotoshare.ThirdPartyAttribute{ + IssuanceToken: []byte(tokenValue), + IssuingAttributes: &yotiprotoshare.IssuingAttributes{ + ExpiryDate: "2006-13-02T15:04:05.000Z", + }, + } + + marshalled, err := proto.Marshal(thirdPartyAttribute) + + assert.NilError(t, err) + + var tokenBytes = []byte(tokenValue) + var expectedBase64Token = base64.StdEncoding.EncodeToString(tokenBytes) + + result, err := ParseIssuanceDetails(marshalled) + assert.Equal(t, expectedBase64Token, result.Token()) + assert.Assert(t, is.Nil(result.ExpiryDate())) + assert.Equal(t, "parsing time \"2006-13-02T15:04:05.000Z\": month out of range", err.Error()) +} + +func TestIssuanceDetails_parseExpiryDate_ShouldParseAllRFC3339Formats(t *testing.T) { + table := []struct { + Input string + Expected time.Time + }{ + { + Input: "2006-01-02T22:04:05Z", + Expected: time.Date(2006, 01, 02, 22, 4, 5, 0, time.UTC), + }, + { + Input: "2010-05-20T10:44:25Z", + Expected: time.Date(2010, 5, 20, 10, 44, 25, 0, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.1Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 100e6, time.UTC), + }, + { + Input: "2012-03-06T04:20:07.5Z", + Expected: time.Date(2012, 3, 6, 4, 20, 7, 500e6, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.12Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 120e6, time.UTC), + }, + { + Input: "2013-03-04T20:43:55.56Z", + Expected: time.Date(2013, 3, 4, 20, 43, 55, 560e6, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.123Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 123e6, time.UTC), + }, + { + Input: "2007-04-07T17:34:11.784Z", + Expected: time.Date(2007, 4, 7, 17, 34, 11, 784e6, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.1234Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 123400e3, time.UTC), + }, + { + Input: "2017-09-14T16:54:30.4784Z", + Expected: time.Date(2017, 9, 14, 16, 54, 30, 478400e3, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.12345Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 123450e3, time.UTC), + }, + { + Input: "2009-06-07T14:20:30.74622Z", + Expected: time.Date(2009, 6, 7, 14, 20, 30, 746220e3, time.UTC), + }, + { + Input: "2006-01-02T22:04:05.123456Z", + Expected: time.Date(2006, 1, 2, 22, 4, 5, 123456e3, time.UTC), + }, + { + Input: "2008-10-25T06:50:55.643562Z", + Expected: time.Date(2008, 10, 25, 6, 50, 55, 643562e3, time.UTC), + }, + { + Input: "2002-10-02T10:00:00-05:00", + Expected: time.Date(2002, 10, 2, 10, 0, 0, 0, time.FixedZone("-0500", -5*60*60)), + }, + { + Input: "2002-10-02T10:00:00+11:00", + Expected: time.Date(2002, 10, 2, 10, 0, 0, 0, time.FixedZone("+1100", 11*60*60)), + }, + { + Input: "1920-03-13T19:50:53.999999Z", + Expected: time.Date(1920, 3, 13, 19, 50, 53, 999999e3, time.UTC), + }, + { + Input: "1920-03-13T19:50:54.000001Z", + Expected: time.Date(1920, 3, 13, 19, 50, 54, 1e3, time.UTC), + }, + } + + for _, row := range table { + func(input string, expected time.Time) { + expiryDate, err := parseExpiryDate(input) + assert.NilError(t, err) + assert.Equal(t, expiryDate.UTC(), expected.UTC()) + }(row.Input, row.Expected) + } +} + +func TestInvalidProtobufThrowsError(t *testing.T) { + result, err := ParseIssuanceDetails([]byte("invalid")) + + assert.Assert(t, is.Nil(result)) + + assert.ErrorContains(t, err, "unable to parse ThirdPartyAttribute value") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/item.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/item.go new file mode 100644 index 0000000..3efd2b9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/item.go @@ -0,0 +1,14 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// Item is a structure which contains information about an attribute value +type Item struct { + // ContentType is the content of the item. + ContentType yotiprotoattr.ContentType + + // Value is the underlying data of the item. + Value interface{} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/json_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/json_attribute.go new file mode 100644 index 0000000..be40920 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/json_attribute.go @@ -0,0 +1,58 @@ +package attribute + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// JSONAttribute is a Yoti attribute which returns an interface as its value +type JSONAttribute struct { + attributeDetails + // value returns the value of a JSON attribute in the form of an interface + value map[string]interface{} +} + +// NewJSON creates a new JSON attribute +func NewJSON(a *yotiprotoattr.Attribute) (*JSONAttribute, error) { + var interfaceValue map[string]interface{} + decoder := json.NewDecoder(bytes.NewReader(a.Value)) + decoder.UseNumber() + err := decoder.Decode(&interfaceValue) + if err != nil { + err = fmt.Errorf("unable to parse JSON value: %q. Error: %q", a.Value, err) + return nil, err + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &JSONAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: interfaceValue, + }, nil +} + +// unmarshallJSON unmarshalls JSON into an interface +func unmarshallJSON(byteValue []byte) (result map[string]interface{}, err error) { + var unmarshalledJSON map[string]interface{} + err = json.Unmarshal(byteValue, &unmarshalledJSON) + + if err != nil { + return nil, err + } + + return unmarshalledJSON, err +} + +// Value returns the value of the JSONAttribute as an interface. +func (a *JSONAttribute) Value() map[string]interface{} { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/json_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/json_attribute_test.go new file mode 100644 index 0000000..4275637 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/json_attribute_test.go @@ -0,0 +1,76 @@ +package attribute + +import ( + "fmt" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func ExampleNewJSON() { + proto := yotiprotoattr.Attribute{ + Name: "exampleJSON", + Value: []byte(`{"foo":"bar"}`), + ContentType: yotiprotoattr.ContentType_JSON, + } + attribute, err := NewJSON(&proto) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + fmt.Println(attribute.Value()) + // Output: map[foo:bar] +} + +func TestNewJSON_ShouldReturnNilForInvalidJSON(t *testing.T) { + proto := yotiprotoattr.Attribute{ + Name: "exampleJSON", + Value: []byte("Not a json document"), + ContentType: yotiprotoattr.ContentType_JSON, + } + attribute, err := NewJSON(&proto) + assert.Check(t, attribute == nil) + assert.ErrorContains(t, err, "unable to parse JSON value") +} + +func TestYotiClient_UnmarshallJSONValue_InvalidValueThrowsError(t *testing.T) { + invalidStructuredAddress := []byte("invalidBool") + + _, err := unmarshallJSON(invalidStructuredAddress) + + assert.Assert(t, err != nil) +} + +func TestYotiClient_UnmarshallJSONValue_ValidValue(t *testing.T) { + const ( + countryIso = "IND" + nestedValue = "NestedValue" + ) + + var structuredAddress = []byte(` + { + "address_format": 2, + "building": "House No.86-A", + "state": "Punjab", + "postal_code": "141012", + "country_iso": "` + countryIso + `", + "country": "India", + "formatted_address": "House No.86-A\nRajgura Nagar\nLudhina\nPunjab\n141012\nIndia", + "1": + { + "1-1": + { + "1-1-1": "` + nestedValue + `" + } + } + } + `) + + parsedStructuredAddress, err := unmarshallJSON(structuredAddress) + assert.NilError(t, err, "Failed to parse structured address") + + actualCountryIso := parsedStructuredAddress["country_iso"] + + assert.Equal(t, countryIso, actualCountryIso) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/multivalue_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/multivalue_attribute.go new file mode 100644 index 0000000..926141f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/multivalue_attribute.go @@ -0,0 +1,90 @@ +package attribute + +import ( + "fmt" + + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" +) + +// MultiValueAttribute is a Yoti attribute which returns a multi-valued attribute +type MultiValueAttribute struct { + attributeDetails + items []*Item +} + +// NewMultiValue creates a new MultiValue attribute +func NewMultiValue(a *yotiprotoattr.Attribute) (*MultiValueAttribute, error) { + attributeItems, err := parseMultiValue(a.Value) + + if err != nil { + return nil, err + } + + return &MultiValueAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: anchor.ParseAnchors(a.Anchors), + id: &a.EphemeralId, + }, + items: attributeItems, + }, nil +} + +// parseMultiValue recursively unmarshals and converts Multi Value bytes into a slice of Items +func parseMultiValue(data []byte) ([]*Item, error) { + var attributeItems []*Item + protoMultiValueStruct, err := unmarshallMultiValue(data) + + if err != nil { + return nil, err + } + + for _, multiValueItem := range protoMultiValueStruct.Values { + var value *Item + if multiValueItem.ContentType == yotiprotoattr.ContentType_MULTI_VALUE { + parsedInnerMultiValueItems, err := parseMultiValue(multiValueItem.Data) + + if err != nil { + return nil, fmt.Errorf("unable to parse multi-value data: %v", err) + } + + value = &Item{ + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Value: parsedInnerMultiValueItems, + } + } else { + itemValue, err := parseValue(multiValueItem.ContentType, multiValueItem.Data) + + if err != nil { + return nil, fmt.Errorf("unable to parse data within a multi-value attribute. Content type: %q, data: %q, error: %v", + multiValueItem.ContentType, multiValueItem.Data, err) + } + + value = &Item{ + ContentType: multiValueItem.ContentType, + Value: itemValue, + } + } + attributeItems = append(attributeItems, value) + } + + return attributeItems, nil +} + +func unmarshallMultiValue(bytes []byte) (*yotiprotoattr.MultiValue, error) { + multiValueStruct := &yotiprotoattr.MultiValue{} + + if err := proto.Unmarshal(bytes, multiValueStruct); err != nil { + return nil, fmt.Errorf("unable to parse MULTI_VALUE value: %q. Error: %q", string(bytes), err) + } + + return multiValueStruct, nil +} + +// Value returns the value of the MultiValueAttribute as a string +func (a *MultiValueAttribute) Value() []*Item { + return a.items +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/multivalue_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/multivalue_attribute_test.go new file mode 100644 index 0000000..15a24f9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/multivalue_attribute_test.go @@ -0,0 +1,157 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func marshallMultiValue(t *testing.T, multiValue *yotiprotoattr.MultiValue) []byte { + marshalled, err := proto.Marshal(multiValue) + + assert.NilError(t, err) + + return marshalled +} + +func createMultiValueAttribute(t *testing.T, multiValueItemSlice []*yotiprotoattr.MultiValue_Value) (*MultiValueAttribute, error) { + var multiValueStruct = &yotiprotoattr.MultiValue{ + Values: multiValueItemSlice, + } + + var marshalledMultiValueData = marshallMultiValue(t, multiValueStruct) + attributeName := "nestedMultiValue" + + var protoAttribute = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: marshalledMultiValueData, + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Anchors: []*yotiprotoattr.Anchor{}, + } + + return NewMultiValue(protoAttribute) +} + +func TestAttribute_MultiValueNotCreatedWithNonMultiValueType(t *testing.T) { + attributeName := "attributeName" + attributeValueString := "value" + attributeValue := []byte(attributeValueString) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + _, err := NewMultiValue(attr) + + assert.Assert(t, err != nil, "Expected error when creating multi value from attribute which isn't of multi-value type") +} + +func TestAttribute_NewMultiValue(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt") + + multiValueAttribute, err := NewMultiValue(protoAttribute) + + assert.NilError(t, err) + + documentImagesAttributeItems, err := CreateImageSlice(multiValueAttribute.Value()) + assert.NilError(t, err) + + assertIsExpectedDocumentImagesAttribute(t, documentImagesAttributeItems, multiValueAttribute.Anchors()[0]) +} + +func TestAttribute_InvalidMultiValueNotReturned(t *testing.T) { + var invalidMultiValueItem = &yotiprotoattr.MultiValue_Value{ + ContentType: yotiprotoattr.ContentType_DATE, + Data: []byte("invalid"), + } + + var stringMultiValueItem = &yotiprotoattr.MultiValue_Value{ + ContentType: yotiprotoattr.ContentType_STRING, + Data: []byte("string"), + } + + var multiValueItemSlice = []*yotiprotoattr.MultiValue_Value{invalidMultiValueItem, stringMultiValueItem} + + var multiValueStruct = &yotiprotoattr.MultiValue{ + Values: multiValueItemSlice, + } + + var marshalledMultiValueData = marshallMultiValue(t, multiValueStruct) + attributeName := "nestedMultiValue" + + var protoAttribute = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: marshalledMultiValueData, + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Anchors: []*yotiprotoattr.Anchor{}, + } + + multiValueAttr, err := NewMultiValue(protoAttribute) + assert.Check(t, err != nil) + + assert.Assert(t, is.Nil(multiValueAttr)) +} + +func TestAttribute_NestedMultiValue(t *testing.T) { + var innerMultiValueProtoValue = createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt").Value + + var stringMultiValueItem = &yotiprotoattr.MultiValue_Value{ + ContentType: yotiprotoattr.ContentType_STRING, + Data: []byte("string"), + } + + var multiValueItem = &yotiprotoattr.MultiValue_Value{ + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Data: innerMultiValueProtoValue, + } + + var multiValueItemSlice = []*yotiprotoattr.MultiValue_Value{stringMultiValueItem, multiValueItem} + + multiValueAttribute, err := createMultiValueAttribute(t, multiValueItemSlice) + + assert.NilError(t, err) + + for key, value := range multiValueAttribute.Value() { + switch key { + case 0: + value0 := value.Value + + assert.Equal(t, value0.(string), "string") + case 1: + value1 := value.Value + + innerItems, ok := value1.([]*Item) + assert.Assert(t, ok) + + for innerKey, item := range innerItems { + switch innerKey { + case 0: + assertIsExpectedImage(t, item.Value.(media.Media), media.ImageTypeJPEG, "vWgD//2Q==") + + case 1: + assertIsExpectedImage(t, item.Value.(media.Media), media.ImageTypeJPEG, "38TVEH/9k=") + } + } + } + } +} + +func TestAttribute_MultiValueGenericGetter(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_multivalue.txt") + multiValueAttribute, err := NewMultiValue(protoAttribute) + assert.NilError(t, err) + + // We need to cast, since GetAttribute always returns generic attributes + multiValueAttributeValue := multiValueAttribute.Value() + imageSlice, err := CreateImageSlice(multiValueAttributeValue) + assert.NilError(t, err) + + assertIsExpectedDocumentImagesAttribute(t, imageSlice, multiValueAttribute.Anchors()[0]) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/parser.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/parser.go new file mode 100644 index 0000000..d663595 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/parser.go @@ -0,0 +1,56 @@ +package attribute + +import ( + "fmt" + "strconv" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +func parseValue(contentType yotiprotoattr.ContentType, byteValue []byte) (interface{}, error) { + switch contentType { + case yotiprotoattr.ContentType_DATE: + parsedTime, err := time.Parse("2006-01-02", string(byteValue)) + + if err == nil { + return &parsedTime, nil + } + + return nil, fmt.Errorf("unable to parse date value: %q. Error: %q", string(byteValue), err) + + case yotiprotoattr.ContentType_JSON: + unmarshalledJSON, err := unmarshallJSON(byteValue) + + if err == nil { + return unmarshalledJSON, nil + } + + return nil, fmt.Errorf("unable to parse JSON value: %q. Error: %q", string(byteValue), err) + + case yotiprotoattr.ContentType_STRING: + return string(byteValue), nil + + case yotiprotoattr.ContentType_MULTI_VALUE: + return parseMultiValue(byteValue) + + case yotiprotoattr.ContentType_INT: + var stringValue = string(byteValue) + intValue, err := strconv.Atoi(stringValue) + if err == nil { + return intValue, nil + } + + return nil, fmt.Errorf("unable to parse INT value: %q. Error: %q", string(byteValue), err) + + case yotiprotoattr.ContentType_JPEG, + yotiprotoattr.ContentType_PNG: + return parseImageValue(contentType, byteValue) + + case yotiprotoattr.ContentType_UNDEFINED: + return string(byteValue), nil + + default: + return string(byteValue), nil + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/parser_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/parser_test.go new file mode 100644 index 0000000..cc9f3d8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/parser_test.go @@ -0,0 +1,16 @@ +package attribute + +import ( + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "gotest.tools/v3/assert" +) + +func TestParseValue_ShouldParseInt(t *testing.T) { + parsed, err := parseValue(yotiprotoattr.ContentType_INT, []byte("7")) + assert.NilError(t, err) + integer, ok := parsed.(int) + assert.Check(t, ok) + assert.Equal(t, integer, 7) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/string_attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/string_attribute.go new file mode 100644 index 0000000..73346b9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/string_attribute.go @@ -0,0 +1,32 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute/anchor" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// StringAttribute is a Yoti attribute which returns a string as its value +type StringAttribute struct { + attributeDetails + value string +} + +// NewString creates a new String attribute +func NewString(a *yotiprotoattr.Attribute) *StringAttribute { + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &StringAttribute{ + attributeDetails: attributeDetails{ + name: a.Name, + contentType: a.ContentType.String(), + anchors: parsedAnchors, + id: &a.EphemeralId, + }, + value: string(a.Value), + } +} + +// Value returns the value of the StringAttribute as a string +func (a *StringAttribute) Value() string { + return a.value +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/string_attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/string_attribute_test.go new file mode 100644 index 0000000..828df20 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/attribute/string_attribute_test.go @@ -0,0 +1,22 @@ +package attribute + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestStringAttribute_NewThirdPartyAttribute(t *testing.T) { + protoAttribute := createAttributeFromTestFile(t, "../../test/fixtures/test_attribute_third_party.txt") + + stringAttribute := NewString(protoAttribute) + + assert.Equal(t, stringAttribute.Value(), "test-third-party-attribute-0") + assert.Equal(t, stringAttribute.Name(), "com.thirdparty.id") + + assert.Equal(t, stringAttribute.Sources()[0].Value(), "THIRD_PARTY") + assert.Equal(t, stringAttribute.Sources()[0].SubType(), "orgName") + + assert.Equal(t, stringAttribute.Verifiers()[0].Value(), "THIRD_PARTY") + assert.Equal(t, stringAttribute.Verifiers()[0].SubType(), "orgName") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/base_profile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/base_profile.go new file mode 100644 index 0000000..df5ad75 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/base_profile.go @@ -0,0 +1,75 @@ +package profile + +import ( + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +type baseProfile struct { + attributeSlice []*yotiprotoattr.Attribute +} + +// GetAttribute retrieve an attribute by name on the Yoti profile. Will return nil if attribute is not present. +func (p baseProfile) GetAttribute(attributeName string) *attribute.GenericAttribute { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + return attribute.NewGeneric(a) + } + } + return nil +} + +// GetAttributeByID retrieve an attribute by ID on the Yoti profile. Will return nil if attribute is not present. +func (p baseProfile) GetAttributeByID(attributeID string) *attribute.GenericAttribute { + for _, a := range p.attributeSlice { + if a.EphemeralId == attributeID { + return attribute.NewGeneric(a) + } + } + return nil +} + +// GetAttributes retrieve a list of attributes by name on the Yoti profile. Will return an empty list of attribute is not present. +func (p baseProfile) GetAttributes(attributeName string) []*attribute.GenericAttribute { + var attributes []*attribute.GenericAttribute + for _, a := range p.attributeSlice { + if a.Name == attributeName { + attributes = append(attributes, attribute.NewGeneric(a)) + } + } + return attributes +} + +// GetStringAttribute retrieves a string attribute by name. Will return nil if attribute is not present. +func (p baseProfile) GetStringAttribute(attributeName string) *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + return attribute.NewString(a) + } + } + return nil +} + +// GetImageAttribute retrieves an image attribute by name. Will return nil if attribute is not present. +func (p baseProfile) GetImageAttribute(attributeName string) *attribute.ImageAttribute { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + imageAttribute, err := attribute.NewImage(a) + + if err == nil { + return imageAttribute + } + } + } + return nil +} + +// GetJSONAttribute retrieves a JSON attribute by name. Will return nil if attribute is not present. +func (p baseProfile) GetJSONAttribute(attributeName string) (*attribute.JSONAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + return attribute.NewJSON(a) + } + } + return nil, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/data_objects.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/data_objects.go new file mode 100644 index 0000000..27d4b65 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/data_objects.go @@ -0,0 +1,27 @@ +package profile + +type receiptDO struct { + ReceiptID string `json:"receipt_id"` + OtherPartyProfileContent string `json:"other_party_profile_content"` + ProfileContent string `json:"profile_content"` + OtherPartyExtraDataContent string `json:"other_party_extra_data_content"` + ExtraDataContent string `json:"extra_data_content"` + WrappedReceiptKey string `json:"wrapped_receipt_key"` + PolicyURI string `json:"policy_uri"` + PersonalKey string `json:"personal_key"` + RememberMeID string `json:"remember_me_id"` + ParentRememberMeID string `json:"parent_remember_me_id"` + SharingOutcome string `json:"sharing_outcome"` + Timestamp string `json:"timestamp"` +} + +type errorDetailsDO struct { + ErrorCode *string `json:"error_code"` + Description *string `json:"description"` +} + +type profileDO struct { + SessionData string `json:"session_data"` + Receipt receiptDO `json:"receipt"` + ErrorDetails *errorDetailsDO `json:"error_details"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/receipt_parser.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/receipt_parser.go new file mode 100644 index 0000000..5fff5a0 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/receipt_parser.go @@ -0,0 +1,61 @@ +package profile + +import ( + "crypto/rsa" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/util" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" + "google.golang.org/protobuf/proto" +) + +func parseApplicationProfile(receipt *receiptDO, key *rsa.PrivateKey) (result *yotiprotoattr.AttributeList, err error) { + decipheredBytes, err := parseEncryptedProto(receipt, receipt.ProfileContent, key) + if err != nil { + return + } + + attributeList := &yotiprotoattr.AttributeList{} + if err := proto.Unmarshal(decipheredBytes, attributeList); err != nil { + return nil, err + } + + return attributeList, nil +} + +func parseUserProfile(receipt *receiptDO, key *rsa.PrivateKey) (result *yotiprotoattr.AttributeList, err error) { + decipheredBytes, err := parseEncryptedProto(receipt, receipt.OtherPartyProfileContent, key) + if err != nil { + return + } + + attributeList := &yotiprotoattr.AttributeList{} + if err := proto.Unmarshal(decipheredBytes, attributeList); err != nil { + return nil, err + } + + return attributeList, nil +} + +func decryptExtraData(receipt *receiptDO, key *rsa.PrivateKey) (result []byte, err error) { + bytes, err := parseEncryptedProto(receipt, receipt.ExtraDataContent, key) + return bytes, err +} + +func parseEncryptedProto(receipt *receiptDO, encryptedBase64 string, key *rsa.PrivateKey) (result []byte, err error) { + unwrappedKey, err := cryptoutil.UnwrapKey(receipt.WrappedReceiptKey, key) + if err != nil { + return + } + encryptedBytes, err := util.Base64ToBytes(encryptedBase64) + if err != nil || len(encryptedBytes) == 0 { + return + } + encryptedData := &yotiprotocom.EncryptedData{} + if err = proto.Unmarshal(encryptedBytes, encryptedData); err != nil { + return + } + + return cryptoutil.DecipherAes(unwrappedKey, encryptedData.Iv, encryptedData.CipherText) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/anchor.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/anchor.go new file mode 100644 index 0000000..ee630c4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/anchor.go @@ -0,0 +1,62 @@ +package sandbox + +import ( + "encoding/json" + "time" +) + +// SourceAnchor initialises an anchor where the type is "SOURCE", +// which has information about how the anchor was sourced. +func SourceAnchor(subtype string, timestamp time.Time, value string) Anchor { + return Anchor{ + Type: "SOURCE", + Value: value, + SubType: subtype, + Timestamp: timestamp, + } +} + +// VerifierAnchor initialises an anchor where the type is "VERIFIER", +// which has information about how the anchor was verified. +func VerifierAnchor(subtype string, timestamp time.Time, value string) Anchor { + return Anchor{ + Type: "VERIFIER", + Value: value, + SubType: subtype, + Timestamp: timestamp, + } +} + +// Anchor is the metadata associated with an attribute. +// It describes how an attribute has been provided to Yoti +// (SOURCE Anchor) and how it has been verified (VERIFIER Anchor). +type Anchor struct { + // Type of the Anchor - most likely either SOURCE or VERIFIER, but it's + // possible that new Anchor types will be added in future. + Type string + // Value identifies the provider that either sourced or verified the attribute value. + // The range of possible values is not limited. For a SOURCE anchor, expect values like + // PASSPORT, DRIVING_LICENSE. For a VERIFIER anchor expect values like YOTI_ADMIN. + Value string + // SubType is an indicator of any specific processing method, or subcategory, + // pertaining to an artifact. For example, for a passport, this would be + // either "NFC" or "OCR". + SubType string + // Timestamp is the time when the anchor was created, i.e. when it was SOURCED or VERIFIED. + Timestamp time.Time +} + +// MarshalJSON returns the JSON encoding +func (anchor *Anchor) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + Value string `json:"value"` + SubType string `json:"sub_type"` + Timestamp int64 `json:"timestamp"` + }{ + Type: anchor.Type, + Value: anchor.Value, + SubType: anchor.SubType, + Timestamp: anchor.Timestamp.UnixNano() / 1e6, + }) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/anchor_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/anchor_test.go new file mode 100644 index 0000000..ee62129 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/anchor_test.go @@ -0,0 +1,32 @@ +package sandbox + +import ( + "fmt" + "time" +) + +func ExampleSourceAnchor() { + time.Local = time.UTC + source := SourceAnchor("subtype", time.Unix(1234567890, 0), "value") + marshalled, err := source.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalled)) + // Output: {"type":"SOURCE","value":"value","sub_type":"subtype","timestamp":1234567890000} +} + +func ExampleVerifierAnchor() { + time.Local = time.UTC + verifier := VerifierAnchor("subtype", time.Unix(1234567890, 0), "value") + marshalled, err := verifier.MarshalJSON() + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalled)) + // Output: {"type":"VERIFIER","value":"value","sub_type":"subtype","timestamp":1234567890000} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/attribute.go new file mode 100644 index 0000000..bf9fbf2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/attribute.go @@ -0,0 +1,52 @@ +package sandbox + +import "strconv" + +// Attribute describes an attribute on a sandbox profile +type Attribute struct { + Name string `json:"name"` + Value string `json:"value"` + Derivation string `json:"derivation"` + Optional string `json:"optional"` + Anchors []Anchor `json:"anchors"` +} + +// Derivation is a builder for derivation strings +type Derivation struct { + value string +} + +// WithName sets the value of a Sandbox Attribute +func (attr Attribute) WithName(name string) Attribute { + attr.Name = name + return attr +} + +// WithValue sets the value of a Sandbox Attribute +func (attr Attribute) WithValue(value string) Attribute { + attr.Value = value + return attr +} + +// WithAnchor sets the Anchor of a Sandbox Attribute +func (attr Attribute) WithAnchor(anchor Anchor) Attribute { + attr.Anchors = append(attr.Anchors, anchor) + return attr +} + +// ToString returns the string representation for a derivation +func (derivation Derivation) ToString() string { + return derivation.value +} + +// AgeOver builds an age over age derivation +func (derivation Derivation) AgeOver(age int) Derivation { + derivation.value = "age_over:" + strconv.Itoa(age) + return derivation +} + +// AgeUnder builds an age under age derivation +func (derivation Derivation) AgeUnder(age int) Derivation { + derivation.value = "age_under:" + strconv.Itoa(age) + return derivation +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/attribute_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/attribute_test.go new file mode 100644 index 0000000..82a2f5b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/attribute_test.go @@ -0,0 +1,48 @@ +package sandbox + +import ( + "fmt" + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func ExampleAttribute_WithAnchor() { + time.Local = time.UTC + attribute := Attribute{}.WithAnchor(SourceAnchor("", time.Unix(1234567890, 0), "")) + fmt.Print(attribute) + // Output: { [{SOURCE 2009-02-13 23:31:30 +0000 UTC}]} +} + +func TestAttribute_WithName(t *testing.T) { + attribute := Attribute{}.WithName("attribute_name") + + assert.Equal(t, attribute.Name, "attribute_name") +} + +func TestAttribute_WithValue(t *testing.T) { + attribute := Attribute{}.WithValue("Value") + + assert.Equal(t, attribute.Value, "Value") +} + +func ExampleDerivation_AgeOver() { + attribute := Attribute{ + Name: "date_of_birth", + Value: "Value", + Derivation: Derivation{}.AgeOver(18).ToString(), + } + fmt.Println(attribute) + // Output: {date_of_birth Value age_over:18 []} +} + +func ExampleDerivation_AgeUnder() { + attribute := Attribute{ + Name: "date_of_birth", + Value: "Value", + Derivation: Derivation{}.AgeUnder(14).ToString(), + } + fmt.Println(attribute) + // Output: {date_of_birth Value age_under:14 []} +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/client.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/client.go new file mode 100644 index 0000000..9cbde3b --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/client.go @@ -0,0 +1,80 @@ +package sandbox + +import ( + "crypto/rsa" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + yotirequest "github.com/getyoti/yoti-go-sdk/v3/requests" +) + +// Client is responsible for setting up test data in the sandbox instance. BaseURL is not required. +type Client struct { + // Client SDK ID. This can be found in the Yoti Hub after you have created and activated an application. + ClientSdkID string + // Private Key associated for your application, can be downloaded from the Yoti Hub. + Key *rsa.PrivateKey + // Base URL to use. This is not required, and a default will be set if not provided. + BaseURL string + // Mockable HTTP Client Interface + HTTPClient interface { + Do(*http.Request) (*http.Response, error) + } +} + +func (client *Client) do(request *http.Request) (*http.Response, error) { + if client.HTTPClient != nil { + return client.HTTPClient.Do(request) + } + return http.DefaultClient.Do(request) +} + +// SetupSharingProfile creates a user profile in the sandbox instance +func (client *Client) SetupSharingProfile(tokenRequest TokenRequest) (token string, err error) { + if client.BaseURL == "" { + if value, exists := os.LookupEnv("YOTI_API_URL"); exists && value != "" { + client.BaseURL = value + } else { + client.BaseURL = "https://api.yoti.com/sandbox/v1" + } + } + + requestEndpoint := "/apps/" + client.ClientSdkID + "/tokens" + requestBody, err := json.Marshal(tokenRequest) + if err != nil { + return + } + + request, err := (&yotirequest.SignedRequest{ + Key: client.Key, + HTTPMethod: http.MethodPost, + BaseURL: client.BaseURL, + Endpoint: requestEndpoint, + Headers: yotirequest.JSONHeaders(), + Body: requestBody, + }).Request() + if err != nil { + return + } + + response, err := client.do(request) + if err != nil { + return + } + if response.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(response.Body) + return "", fmt.Errorf("Sharing Profile not created (HTTP %d) %s", response.StatusCode, string(body)) + } + + responseStruct := struct { + Token string `json:"token"` + }{} + + err = json.NewDecoder(response.Body).Decode(&responseStruct) + token = responseStruct.Token + + return +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/client_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/client_test.go new file mode 100644 index 0000000..0507083 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/client_test.go @@ -0,0 +1,145 @@ +package sandbox + +import ( + "crypto/rand" + "crypto/rsa" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "gotest.tools/v3/assert" +) + +func TestClient_SetupSharingProfile_ShouldReturnErrorIfProfileNotCreated(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + client := Client{ + Key: key, + BaseURL: "example.com", + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 401, + Body: io.NopCloser(strings.NewReader("")), + }, nil + }, + }, + } + _, err = client.SetupSharingProfile(TokenRequest{}) + assert.ErrorContains(t, err, "Sharing Profile not created") +} + +func TestClient_SetupSharingProfile_Success(t *testing.T) { + expectedToken := "shareToken" + key, err := rsa.GenerateKey(rand.Reader, 1024) + assert.NilError(t, err) + + client := Client{ + Key: key, + BaseURL: "example.com", + HTTPClient: &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{"token":"` + expectedToken + `"}`)), + }, nil + }, + }, + } + token, err := client.SetupSharingProfile(TokenRequest{}) + assert.NilError(t, err) + + assert.Equal(t, token, expectedToken) +} +func TestClient_SetupSharingProfileUsesConstructorBaseUrlOverEnvVariable(t *testing.T) { + client := createSandboxClient(t, "constuctorBaseUrl") + os.Setenv("YOTI_API_URL", "envBaseUrl") + + _, err := client.SetupSharingProfile(TokenRequest{}) + assert.NilError(t, err) + + assert.Equal(t, "constuctorBaseUrl", client.BaseURL) +} + +func TestClient_SetupSharingProfileUsesEnvVariable(t *testing.T) { + client := createSandboxClient(t, "") + + os.Setenv("YOTI_API_URL", "envBaseUrl") + + _, err := client.SetupSharingProfile(TokenRequest{}) + assert.NilError(t, err) + + assert.Equal(t, "envBaseUrl", client.BaseURL) +} + +func TestClient_SetupSharingProfileUsesDefaultUrlAsFallbackWithEmptyEnvValue(t *testing.T) { + os.Setenv("YOTI_API_URL", "") + + client := createSandboxClient(t, "") + + _, err := client.SetupSharingProfile(TokenRequest{}) + assert.NilError(t, err) + + assert.Equal(t, "https://api.yoti.com/sandbox/v1", client.BaseURL) +} + +func TestClient_SetupSharingProfileUsesDefaultUrlAsFallbackWithNoEnvValue(t *testing.T) { + os.Unsetenv("YOTI_API_URL") + + client := createSandboxClient(t, "") + + _, err := client.SetupSharingProfile(TokenRequest{}) + assert.NilError(t, err) + + assert.Equal(t, "https://api.yoti.com/sandbox/v1", client.BaseURL) +} + +func createSandboxClient(t *testing.T, constructorBaseUrl string) (client Client) { + keyBytes, fileErr := os.ReadFile("../../test/test-key.pem") + assert.NilError(t, fileErr) + + pemFile, parseErr := cryptoutil.ParseRSAKey(keyBytes) + assert.NilError(t, parseErr) + + if constructorBaseUrl == "" { + return Client{ + Key: pemFile, + ClientSdkID: "ClientSDKID", + HTTPClient: mockHttpClientCreatedResponse(), + } + } + + return Client{ + Key: pemFile, + BaseURL: constructorBaseUrl, + ClientSdkID: "ClientSDKID", + HTTPClient: mockHttpClientCreatedResponse(), + } + +} + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func mockHttpClientCreatedResponse() *mockHTTPClient { + return &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 201, + Body: io.NopCloser(strings.NewReader(`{"token":"tokenValue"}`)), + }, nil + }, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/document_images.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/document_images.go new file mode 100644 index 0000000..b331d05 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/document_images.go @@ -0,0 +1,40 @@ +package sandbox + +import ( + "strings" + + "github.com/getyoti/yoti-go-sdk/v3/media" +) + +// DocumentImages describes a Document Images attribute on a sandbox profile +type DocumentImages struct { + Images []media.Media +} + +func (d DocumentImages) getValue() string { + var imageUrls []string + + for _, i := range d.Images { + imageUrls = append(imageUrls, i.Base64URL()) + } + + return strings.Join(imageUrls, "&") +} + +// WithPngImage adds a PNG image to the slice of document images +func (d DocumentImages) WithPngImage(imageContent []byte) DocumentImages { + pngImage := media.PNGImage(imageContent) + + d.Images = append(d.Images, pngImage) + + return d +} + +// WithJpegImage adds a JPEG image to the slice of document images +func (d DocumentImages) WithJpegImage(imageContent []byte) DocumentImages { + jpegImage := media.JPEGImage(imageContent) + + d.Images = append(d.Images, jpegImage) + + return d +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/document_images_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/document_images_test.go new file mode 100644 index 0000000..baa3853 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/document_images_test.go @@ -0,0 +1,47 @@ +package sandbox + +import ( + "fmt" + "testing" + + "github.com/getyoti/yoti-go-sdk/v3/media" + "gotest.tools/v3/assert" +) + +const expectedBase64Content = "3q2+7w==" + +func TestShouldAddJpegImage(t *testing.T) { + documentImages := DocumentImages{}.WithJpegImage([]byte{0xDE, 0xAD, 0xBE, 0xEF}) + + documentImage := documentImages.Images[0] + assert.Equal(t, media.ImageTypeJPEG, documentImage.MIME()) + assert.Equal( + t, + documentImages.getValue(), + fmt.Sprintf("data:image/jpeg;base64,%s", expectedBase64Content)) +} + +func TestShouldAddPngImage(t *testing.T) { + documentImages := DocumentImages{}.WithPngImage([]byte{0xDE, 0xAD, 0xBE, 0xEF}) + + documentImage := documentImages.Images[0] + assert.Equal(t, media.ImageTypePNG, documentImage.MIME()) + assert.Equal( + t, + documentImages.getValue(), + fmt.Sprintf("data:image/png;base64,%s", expectedBase64Content)) +} + +func TestShouldAddMultipleImage(t *testing.T) { + documentImages := DocumentImages{}. + WithPngImage([]byte{0xDE, 0xAD, 0xBE, 0xEF}). + WithPngImage([]byte{0xDE, 0xAD, 0xBE, 0xEF}). + WithJpegImage([]byte{0xDE, 0xAD, 0xBE, 0xEF}) + + assert.Equal(t, 3, len(documentImages.Images)) + + assert.Equal( + t, + documentImages.getValue(), + fmt.Sprintf("data:image/png;base64,%[1]s&data:image/png;base64,%[1]s&data:image/jpeg;base64,%[1]s", expectedBase64Content)) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/tokenrequest.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/tokenrequest.go new file mode 100644 index 0000000..5010c82 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/tokenrequest.go @@ -0,0 +1,133 @@ +package sandbox + +import ( + "encoding/base64" + "encoding/json" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/consts" +) + +// TokenRequest describes a sandbox token request +type TokenRequest struct { + RememberMeID string `json:"remember_me_id"` + Attributes []Attribute `json:"profile_attributes"` +} + +// WithRememberMeID adds the Remember Me ID to the returned ActivityDetails. +// The value returned in ActivityDetails will be the Base64 encoded value of the string specified here. +func (t TokenRequest) WithRememberMeID(rememberMeId string) TokenRequest { + t.RememberMeID = rememberMeId + return t +} + +// WithAttribute adds a new attribute to the sandbox token request +func (t TokenRequest) WithAttribute(name, value string, anchors []Anchor) TokenRequest { + if anchors == nil { + anchors = make([]Anchor, 0) + } + attribute := Attribute{ + Name: name, + Value: value, + Anchors: anchors, + } + + return t.WithAttributeStruct(attribute) +} + +// WithAttributeStruct adds a new attribute struct to the sandbox token request +func (t TokenRequest) WithAttributeStruct(attribute Attribute) TokenRequest { + t.Attributes = append(t.Attributes, attribute) + return t +} + +// WithGivenNames adds given names to the sandbox token request +func (t TokenRequest) WithGivenNames(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrGivenNames, value, anchors) +} + +// WithFamilyName adds a family name to the sandbox token request +func (t TokenRequest) WithFamilyName(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrFamilyName, value, anchors) +} + +// WithFullName adds a full name to the sandbox token request +func (t TokenRequest) WithFullName(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrFullName, value, anchors) +} + +// WithDateOfBirth adds a date of birth to the sandbox token request +func (t TokenRequest) WithDateOfBirth(value time.Time, anchors []Anchor) TokenRequest { + formattedTime := value.Format("2006-01-02") + return t.WithAttribute(consts.AttrDateOfBirth, formattedTime, anchors) +} + +// WithAgeVerification adds an age-based derivation attribute to the sandbox token request +func (t TokenRequest) WithAgeVerification(dateOfBirth time.Time, derivation Derivation, anchors []Anchor) TokenRequest { + if anchors == nil { + anchors = []Anchor{} + } + attribute := Attribute{ + Name: consts.AttrDateOfBirth, + Value: dateOfBirth.Format("2006-01-02"), + Derivation: derivation.ToString(), + Anchors: anchors, + } + t.Attributes = append(t.Attributes, attribute) + return t +} + +// WithGender adds a gender to the sandbox token request +func (t TokenRequest) WithGender(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrGender, value, anchors) +} + +// WithPhoneNumber adds a phone number to the sandbox token request +func (t TokenRequest) WithPhoneNumber(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrMobileNumber, value, anchors) +} + +// WithNationality adds a nationality to the sandbox token request +func (t TokenRequest) WithNationality(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrNationality, value, anchors) +} + +// WithPostalAddress adds a formatted address to the sandbox token request +func (t TokenRequest) WithPostalAddress(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrAddress, value, anchors) +} + +// WithStructuredPostalAddress adds a JSON address to the sandbox token request +func (t TokenRequest) WithStructuredPostalAddress(value map[string]interface{}, anchors []Anchor) TokenRequest { + data, _ := json.Marshal(value) + return t.WithAttribute(consts.AttrStructuredPostalAddress, string(data), anchors) +} + +// WithSelfie adds a selfie image to the sandbox token request +func (t TokenRequest) WithSelfie(value []byte, anchors []Anchor) TokenRequest { + return t.WithBase64Selfie(base64.StdEncoding.EncodeToString(value), anchors) +} + +// WithBase64Selfie adds a base 64 selfie image to the sandbox token request +func (t TokenRequest) WithBase64Selfie(base64Value string, anchors []Anchor) TokenRequest { + return t.WithAttribute( + consts.AttrSelfie, + base64Value, + anchors, + ) +} + +// WithEmailAddress adds an email address to the sandbox token request +func (t TokenRequest) WithEmailAddress(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrEmailAddress, value, anchors) +} + +// WithDocumentDetails adds a document details string to the sandbox token request +func (t TokenRequest) WithDocumentDetails(value string, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrDocumentDetails, value, anchors) +} + +// WithDocumentImages adds document images to the sandbox token request +func (t TokenRequest) WithDocumentImages(value DocumentImages, anchors []Anchor) TokenRequest { + return t.WithAttribute(consts.AttrDocumentImages, value.getValue(), anchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/tokenrequest_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/tokenrequest_test.go new file mode 100644 index 0000000..b789133 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/sandbox/tokenrequest_test.go @@ -0,0 +1,198 @@ +package sandbox + +import ( + "encoding/json" + "fmt" + "time" +) + +func AnchorList() []Anchor { + return []Anchor{ + SourceAnchor("", time.Unix(1234567890, 0), ""), + VerifierAnchor("", time.Unix(1234567890, 0), ""), + } +} + +func ExampleTokenRequest_WithRememberMeID() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithRememberMeID("some-remember-me-id") + + printJson(tokenRequest) + // Output: {"remember_me_id":"some-remember-me-id","profile_attributes":null} +} + +func ExampleTokenRequest_WithAttribute() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithAttribute( + "AttributeName1", + "Value", + AnchorList(), + ).WithAttribute( + "AttributeName2", + "Value", + nil, + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"AttributeName1","value":"Value","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]},{"name":"AttributeName2","value":"Value","derivation":"","optional":"","anchors":[]}]} +} + +func ExampleTokenRequest_WithAttributeStruct() { + attribute := Attribute{ + Name: "AttributeName3", + Value: "Value3", + Anchors: AnchorList(), + } + + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithAttributeStruct(attribute) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"AttributeName3","value":"Value3","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithGivenNames() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithGivenNames( + "Value", + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"given_names","value":"Value","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithFamilyName() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithFamilyName( + "Value", + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"family_name","value":"Value","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithFullName() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithFullName( + "Value", + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"full_name","value":"Value","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithDateOfBirth() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithDateOfBirth(time.Unix(1234567890, 0), AnchorList()) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"date_of_birth","value":"2009-02-13","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithAgeVerification() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithAgeVerification( + time.Unix(1234567890, 0), + Derivation{}.AgeOver(18), + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"date_of_birth","value":"2009-02-13","derivation":"age_over:18","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithGender() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithGender("male", AnchorList()) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"gender","value":"male","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithPhoneNumber() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithPhoneNumber("00005550000", AnchorList()) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"phone_number","value":"00005550000","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithNationality() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithNationality("Value", AnchorList()) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"nationality","value":"Value","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithPostalAddress() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithPostalAddress("Value", AnchorList()) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"postal_address","value":"Value","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithStructuredPostalAddress() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithStructuredPostalAddress( + map[string]interface{}{ + "FormattedAddressLine": "Value", + }, + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"structured_postal_address","value":"{\"FormattedAddressLine\":\"Value\"}","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithSelfie() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithSelfie( + []byte{0xDE, 0xAD, 0xBE, 0xEF}, + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"selfie","value":"3q2+7w==","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithBase64Selfie() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithBase64Selfie( + "3q2+7w==", + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"selfie","value":"3q2+7w==","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithEmailAddress() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithEmailAddress("user@example.com", AnchorList()) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"email_address","value":"user@example.com","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithDocumentDetails() { + time.Local = time.UTC + tokenRequest := TokenRequest{}.WithDocumentDetails( + "DRIVING_LICENCE - abc1234", + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"document_details","value":"DRIVING_LICENCE - abc1234","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func ExampleTokenRequest_WithDocumentImages() { + time.Local = time.UTC + + documentImages := DocumentImages{}.WithPngImage([]byte{0xDE, 0xAD, 0xBE, 0xEF}).WithJpegImage([]byte{0xDE, 0xAD, 0xBE, 0xEF}) + + tokenRequest := TokenRequest{}.WithDocumentImages( + documentImages, + AnchorList(), + ) + printJson(tokenRequest) + // Output: {"remember_me_id":"","profile_attributes":[{"name":"document_images","value":"data:image/png;base64,3q2+7w==\u0026data:image/jpeg;base64,3q2+7w==","derivation":"","optional":"","anchors":[{"type":"SOURCE","value":"","sub_type":"","timestamp":1234567890000},{"type":"VERIFIER","value":"","sub_type":"","timestamp":1234567890000}]}]} +} + +func printJson(value interface{}) { + marshalledJSON, err := json.Marshal(value) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(marshalledJSON)) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/service.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/service.go new file mode 100644 index 0000000..3f9ae50 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/service.go @@ -0,0 +1,145 @@ +package profile + +import ( + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/extra" + "github.com/getyoti/yoti-go-sdk/v3/requests" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +func getProfileEndpoint(token, sdkID string) string { + return fmt.Sprintf("/profile/%s?appId=%s", token, sdkID) +} + +// GetActivityDetails requests information about a Yoti user using the one time +// use token generated by the Yoti login process. Don't call this directly, use yoti.GetActivityDetails +func GetActivityDetails(httpClient requests.HttpClient, token, clientSdkId, apiUrl string, key *rsa.PrivateKey) (activity ActivityDetails, err error) { + if len(token) < 1 { + return activity, yotierror.InvalidTokenError + } + + var decryptedToken string + decryptedToken, err = cryptoutil.DecryptToken(token, key) + if err != nil { + return activity, yotierror.TokenDecryptError + } + + headers := requests.AuthKeyHeader(&key.PublicKey) + + request, err := requests.SignedRequest{ + Key: key, + HTTPMethod: http.MethodGet, + BaseURL: apiUrl, + Endpoint: getProfileEndpoint(decryptedToken, clientSdkId), + Headers: headers, + Body: nil, + }.Request() + if err != nil { + return + } + + response, err := requests.Execute(httpClient, request, map[int]string{404: "Profile not found"}, yotierror.DefaultHTTPErrorMessages) + if err != nil { + return activity, err + } + + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return + } + + return handleSuccessfulResponse(responseBytes, key) +} + +func handleSuccessfulResponse(responseBytes []byte, key *rsa.PrivateKey) (activityDetails ActivityDetails, err error) { + var parsedResponse = profileDO{} + + if err = json.Unmarshal(responseBytes, &parsedResponse); err != nil { + return + } + + if parsedResponse.Receipt.SharingOutcome != "SUCCESS" { + return activityDetails, handleUnsuccessfulShare(parsedResponse) + } + + var userAttributeList, applicationAttributeList *yotiprotoattr.AttributeList + if userAttributeList, err = parseUserProfile(&parsedResponse.Receipt, key); err != nil { + return + } + if applicationAttributeList, err = parseApplicationProfile(&parsedResponse.Receipt, key); err != nil { + return + } + id := parsedResponse.Receipt.RememberMeID + + userProfile := newUserProfile(userAttributeList) + applicationProfile := newApplicationProfile(applicationAttributeList) + + var extraData *extra.Data + extraData, err = parseExtraData(&parsedResponse.Receipt, key, err) + + timestamp, timestampErr := time.Parse(time.RFC3339Nano, parsedResponse.Receipt.Timestamp) + if timestampErr != nil { + err = yotierror.MultiError{This: errors.New("Unable to read timestamp. Error: " + timestampErr.Error()), Next: err} + } + + activityDetails = ActivityDetails{ + UserProfile: userProfile, + rememberMeID: id, + parentRememberMeID: parsedResponse.Receipt.ParentRememberMeID, + timestamp: timestamp, + receiptID: parsedResponse.Receipt.ReceiptID, + ApplicationProfile: applicationProfile, + extraData: extraData, + } + + return activityDetails, err +} + +func parseExtraData(receipt *receiptDO, key *rsa.PrivateKey, err error) (*extra.Data, error) { + decryptedExtraData, decryptErr := decryptExtraData(receipt, key) + if decryptErr != nil { + err = yotierror.MultiError{This: errors.New("Unable to decrypt ExtraData from the receipt. Error: " + decryptErr.Error()), Next: err} + } + + extraData, parseErr := extra.NewExtraData(decryptedExtraData) + if parseErr != nil { + err = yotierror.MultiError{This: errors.New("Unable to parse ExtraData from the receipt. Error: " + parseErr.Error()), Next: err} + } + return extraData, err +} + +func parseIsAgeVerifiedValue(byteValue []byte) (result *bool, err error) { + stringValue := string(byteValue) + + var parseResult bool + parseResult, err = strconv.ParseBool(stringValue) + + if err != nil { + return nil, err + } + + result = &parseResult + + return +} + +func handleUnsuccessfulShare(parsedResponse profileDO) error { + if parsedResponse.ErrorDetails != nil && parsedResponse.ErrorDetails.ErrorCode != nil { + return yotierror.DetailedSharingFailureError{ + Code: parsedResponse.ErrorDetails.ErrorCode, + Description: parsedResponse.ErrorDetails.Description, + } + } + + return yotierror.SharingFailureError +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/service_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/service_test.go new file mode 100644 index 0000000..cc07c0f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/service_test.go @@ -0,0 +1,465 @@ +package profile + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "io" + "net/http" + "net/url" + "os" + "strings" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" + "github.com/getyoti/yoti-go-sdk/v3/yotierror" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotocom" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare" + "google.golang.org/protobuf/proto" + + "github.com/getyoti/yoti-go-sdk/v3/test" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func TestProfileService_ParseIsAgeVerifiedValue_True(t *testing.T) { + trueValue := []byte("true") + + isAgeVerified, err := parseIsAgeVerifiedValue(trueValue) + assert.NilError(t, err, "Failed to parse IsAgeVerified value") + assert.Check(t, *isAgeVerified) +} + +func TestProfileService_ParseIsAgeVerifiedValue_False(t *testing.T) { + falseValue := []byte("false") + + isAgeVerified, err := parseIsAgeVerifiedValue(falseValue) + assert.NilError(t, err, "Failed to parse IsAgeVerified value") + assert.Check(t, !*isAgeVerified) + +} +func TestProfileService_ParseIsAgeVerifiedValue_InvalidValueThrowsError(t *testing.T) { + invalidValue := []byte("invalidBool") + + _, err := parseIsAgeVerifiedValue(invalidValue) + + assert.Assert(t, err != nil) +} + +func TestProfileService_ErrIsThrownForInvalidToken(t *testing.T) { + _, err := GetActivityDetails(nil, "invalidToken", "clientSdkId", "apiUrl", getValidKey()) + + assert.ErrorContains(t, err, "unable to decrypt token") +} + +func TestProfileService_RequestErrIsReturned(t *testing.T) { + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 404, + }, nil + }, + } + _, err := GetActivityDetails(client, test.EncryptedToken, "clientSdkId", "https://apiUrl", getValidKey()) + + assert.ErrorContains(t, err, "404: Profile not found") +} + +func TestProfileService_InvalidToken(t *testing.T) { + _, err := GetActivityDetails(nil, "", "sdkId", "https://apiurl", getValidKey()) + assert.ErrorContains(t, err, "invalid token") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestProfileService_ParseExtraData_ErrorDecrypting(t *testing.T) { + receipt := &receiptDO{ + ExtraDataContent: "invalidExtraData", + } + _, err := parseExtraData(receipt, getValidKey(), nil) + + assert.ErrorContains(t, err, "Unable to decrypt ExtraData from the receipt.") +} + +func TestProfileService_GetActivityDetails(t *testing.T) { + key := getValidKey() + + otherPartyProfileContent := "ChCZAib1TBm9Q5GYfFrS1ep9EnAwQB5shpAPWLBgZgFgt6bCG3S5qmZHhrqUbQr3yL6yeLIDwbM7x4nuT/MYp+LDXgmFTLQNYbDTzrEzqNuO2ZPn9Kpg+xpbm9XtP7ZLw3Ep2BCmSqtnll/OdxAqLb4DTN4/wWdrjnFC+L/oQEECu646" + rememberMeID := "remember_me_id0123456789" + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `","other_party_profile_content": "` + otherPartyProfileContent + `","remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)), + }, nil + }, + } + + activityDetails, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + assert.NilError(t, err) + + profile := activityDetails.UserProfile + + assert.Equal(t, activityDetails.RememberMeID(), rememberMeID) + assert.Assert(t, is.Nil(activityDetails.ExtraData().AttributeIssuanceDetails())) + + expectedSelfieValue := "selfie0123456789" + + assert.DeepEqual(t, profile.Selfie().Value().Data(), []byte(expectedSelfieValue)) + assert.Equal(t, profile.MobileNumber().Value(), "phone_number0123456789") + + assert.Equal( + t, + profile.GetAttribute("phone_number").Value(), + "phone_number0123456789", + ) + + assert.Check(t, + profile.GetImageAttribute("doesnt_exist") == nil, + ) + + assert.Check(t, profile.GivenNames() == nil) + assert.Check(t, profile.FamilyName() == nil) + assert.Check(t, profile.FullName() == nil) + assert.Check(t, profile.EmailAddress() == nil) + images, err := profile.DocumentImages() + assert.NilError(t, err) + assert.Check(t, images == nil) + documentDetails, err := profile.DocumentDetails() + assert.NilError(t, err) + assert.Check(t, documentDetails == nil) + + expectedDoB := time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC) + + actualDoB, err := profile.DateOfBirth() + assert.NilError(t, err) + + assert.Assert(t, actualDoB != nil) + assert.DeepEqual(t, actualDoB.Value(), &expectedDoB) +} + +func TestProfileService_SharingFailure_ReturnsSpecificFailure(t *testing.T) { + key := getValidKey() + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"},"error_details":{"error_code":"SOME_ERROR","description":"SOME_DESCRIPTION"}}`)), + }, nil + }, + } + + errorCode := "SOME_ERROR" + + description := "SOME_DESCRIPTION" + + expectedError := yotierror.DetailedSharingFailureError{ + Code: &errorCode, + Description: &description, + } + + _, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + + assert.DeepEqual(t, err, expectedError) + + assert.ErrorContains(t, err, "sharing failure") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestProfileService_SharingFailure_ReturnsGenericErrorWhenErrorCodeIsNull(t *testing.T) { + key := getValidKey() + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"},"error_details":{}}`)), + }, nil + }, + } + + expectedError := yotierror.DetailedSharingFailureError{ + Code: nil, + Description: nil, + } + + _, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + + assert.DeepEqual(t, err, expectedError) + + assert.ErrorContains(t, err, "sharing failure") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestProfileService_SharingFailure_ReturnsGenericFailure(t *testing.T) { + key := getValidKey() + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"}}`)), + }, nil + }, + } + _, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + + assert.ErrorContains(t, err, "sharing failure") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, !temporary || !tempError.Temporary()) +} + +func TestProfileService_TokenDecodedSuccessfully(t *testing.T) { + key := getValidKey() + + expectedPath := "/profile/" + test.Token + + client := &mockHTTPClient{ + do: func(request *http.Request) (*http.Response, error) { + parsed, err := url.Parse(request.URL.String()) + assert.NilError(t, err, "Yoti API did not generate a valid URI.") + assert.Equal(t, parsed.Path, expectedPath, "Yoti API did not generate a valid URL path.") + + return &http.Response{ + StatusCode: 500, + }, nil + }, + } + + _, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + assert.ErrorContains(t, err, "unknown HTTP error") + + tempError, temporary := err.(interface { + Temporary() bool + }) + assert.Check(t, temporary && tempError.Temporary()) +} + +func TestProfileService_ParentRememberMeID(t *testing.T) { + key := getValidKey() + + otherPartyProfileContent := "ChCZAib1TBm9Q5GYfFrS1ep9EnAwQB5shpAPWLBgZgFgt6bCG3S5qmZHhrqUbQr3yL6yeLIDwbM7x4nuT/MYp+LDXgmFTLQNYbDTzrEzqNuO2ZPn9Kpg+xpbm9XtP7ZLw3Ep2BCmSqtnll/OdxAqLb4DTN4/wWdrjnFC+L/oQEECu646" + parentRememberMeID := "parent_remember_me_id0123456789" + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + + `","other_party_profile_content": "` + otherPartyProfileContent + + `","parent_remember_me_id":"` + parentRememberMeID + + `", "sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)), + }, nil + }, + } + + activityDetails, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + + assert.NilError(t, err) + assert.Equal(t, activityDetails.ParentRememberMeID(), parentRememberMeID) +} +func TestProfileService_ParseWithoutProfile_Success(t *testing.T) { + key := getValidKey() + + rememberMeID := "remember_me_id0123456789" + timestamp := time.Date(1973, 11, 29, 9, 33, 9, 0, time.UTC) + timestampString := func(a []byte, _ error) string { + return string(a) + }(timestamp.MarshalText()) + receiptID := "receipt_id123" + + var otherPartyProfileContents = []string{ + `"other_party_profile_content": null,`, + `"other_party_profile_content": "",`, + ``} + + for _, otherPartyProfileContent := range otherPartyProfileContents { + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `",` + + otherPartyProfileContent + `"remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS", "timestamp":"` + timestampString + `", "receipt_id":"` + receiptID + `"}}`)), + }, nil + }, + } + + activityDetails, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + + assert.NilError(t, err) + assert.Equal(t, activityDetails.RememberMeID(), rememberMeID) + assert.Equal(t, activityDetails.Timestamp(), timestamp) + assert.Equal(t, activityDetails.ReceiptID(), receiptID) + } +} + +func TestProfileService_ShouldParseAndDecryptExtraDataContent(t *testing.T) { + otherPartyProfileContent := "ChCZAib1TBm9Q5GYfFrS1ep9EnAwQB5shpAPWLBgZgFgt6bCG3S5qmZHhrqUbQr3yL6yeLIDwbM7x4nuT/MYp+LDXgmFTLQNYbDTzrEzqNuO2ZPn9Kpg+xpbm9XtP7ZLw3Ep2BCmSqtnll/OdxAqLb4DTN4/wWdrjnFC+L/oQEECu646" + rememberMeID := "remember_me_id0123456789" + + pemBytes, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + dataEntries := make([]*yotiprotoshare.DataEntry, 0) + expiryDate := time.Now().UTC().AddDate(0, 0, 1) + thirdPartyAttributeDataEntry := test.CreateThirdPartyAttributeDataEntry(t, &expiryDate, []string{attributeName}, "tokenValue") + + dataEntries = append(dataEntries, &thirdPartyAttributeDataEntry) + protoExtraData := &yotiprotoshare.ExtraData{ + List: dataEntries, + } + + extraDataContent := createExtraDataContent(t, protoExtraData) + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + + test.WrappedReceiptKey + `","other_party_profile_content": "` + otherPartyProfileContent + `","extra_data_content": "` + + extraDataContent + `","remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)), + }, nil + }, + } + key, err := cryptoutil.ParseRSAKey(pemBytes) + assert.NilError(t, err) + + activityDetails, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + assert.NilError(t, err) + + assert.Equal(t, rememberMeID, activityDetails.RememberMeID()) + assert.Assert(t, activityDetails.ExtraData().AttributeIssuanceDetails() != nil) + assert.Equal(t, activityDetails.UserProfile.MobileNumber().Value(), "phone_number0123456789") +} + +func TestProfileService_ShouldCarryOnProcessingIfIssuanceTokenIsNotPresent(t *testing.T) { + dataEntries := make([]*yotiprotoshare.DataEntry, 0) + expiryDate := time.Now().UTC().AddDate(0, 0, 1) + thirdPartyAttributeDataEntry := test.CreateThirdPartyAttributeDataEntry(t, &expiryDate, []string{attributeName}, "") + + dataEntries = append(dataEntries, &thirdPartyAttributeDataEntry) + protoExtraData := &yotiprotoshare.ExtraData{ + List: dataEntries, + } + + pemBytes, err := os.ReadFile("../test/test-key.pem") + assert.NilError(t, err) + + extraDataContent := createExtraDataContent(t, protoExtraData) + + otherPartyProfileContent := "ChCZAib1TBm9Q5GYfFrS1ep9EnAwQB5shpAPWLBgZgFgt6bCG3S5qmZHhrqUbQr3yL6yeLIDwbM7x4nuT/MYp+LDXgmFTLQNYbDTzrEzqNuO2ZPn9Kpg+xpbm9XtP7ZLw3Ep2BCmSqtnll/OdxAqLb4DTN4/wWdrjnFC+L/oQEECu646" + + rememberMeID := "remember_me_id0123456789" + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + + test.WrappedReceiptKey + `","other_party_profile_content": "` + otherPartyProfileContent + `","extra_data_content": "` + + extraDataContent + `","remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS"}}`)), + }, nil + }, + } + + key, err := cryptoutil.ParseRSAKey(pemBytes) + assert.NilError(t, err) + + activityDetails, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", key) + + assert.Check(t, err != nil) + assert.Check(t, strings.Contains(err.Error(), "Issuance Token is invalid")) + + assert.Equal(t, rememberMeID, activityDetails.RememberMeID()) + assert.Assert(t, is.Nil(activityDetails.ExtraData().AttributeIssuanceDetails())) + assert.Equal(t, activityDetails.UserProfile.MobileNumber().Value(), "phone_number0123456789") +} +func TestProfileService_ParseWithoutRememberMeID_Success(t *testing.T) { + var otherPartyProfileContents = []string{ + `"other_party_profile_content": null,`, + `"other_party_profile_content": "",`} + + for _, otherPartyProfileContent := range otherPartyProfileContents { + + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"receipt":{"wrapped_receipt_key": "` + test.WrappedReceiptKey + `",` + + otherPartyProfileContent + `"sharing_outcome":"SUCCESS", "timestamp":"2006-01-02T15:04:05.999999Z"}}`)), + }, nil + }, + } + _, err := GetActivityDetails(client, test.EncryptedToken, "sdkId", "https://apiurl", getValidKey()) + + assert.NilError(t, err) + } +} + +func getValidKey() *rsa.PrivateKey { + return test.GetValidKey("../test/test-key.pem") +} + +func createExtraDataContent(t *testing.T, protoExtraData *yotiprotoshare.ExtraData) string { + outBytes, err := proto.Marshal(protoExtraData) + assert.NilError(t, err) + + key := getValidKey() + assert.NilError(t, err) + + cipherBytes, err := base64.StdEncoding.DecodeString(test.WrappedReceiptKey) + assert.NilError(t, err) + unwrappedKey, err := rsa.DecryptPKCS1v15(rand.Reader, key, cipherBytes) + assert.NilError(t, err) + cipherBlock, err := aes.NewCipher(unwrappedKey) + assert.NilError(t, err) + + padLength := cipherBlock.BlockSize() - len(outBytes)%cipherBlock.BlockSize() + outBytes = append(outBytes, bytes.Repeat([]byte{byte(padLength)}, padLength)...) + + iv := make([]byte, cipherBlock.BlockSize()) + encrypter := cipher.NewCBCEncrypter(cipherBlock, iv) + encrypter.CryptBlocks(outBytes, outBytes) + + outProto := &yotiprotocom.EncryptedData{ + CipherText: outBytes, + Iv: iv, + } + outBytes, err = proto.Marshal(outProto) + assert.NilError(t, err) + + return base64.StdEncoding.EncodeToString(outBytes) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/user_profile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/user_profile.go new file mode 100644 index 0000000..3ab7648 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/user_profile.go @@ -0,0 +1,182 @@ +package profile + +import ( + "strings" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/profile/attribute" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" +) + +// UserProfile represents the details retrieved for a particular user. Consists of +// Yoti attributes: a small piece of information about a Yoti user such as a +// photo of the user or the user's date of birth. +type UserProfile struct { + baseProfile +} + +// Creates a new Profile struct +func newUserProfile(attributes *yotiprotoattr.AttributeList) UserProfile { + return UserProfile{ + baseProfile{ + attributeSlice: createAttributeSlice(attributes), + }, + } +} + +func createAttributeSlice(protoAttributeList *yotiprotoattr.AttributeList) (result []*yotiprotoattr.Attribute) { + if protoAttributeList != nil { + result = append(result, protoAttributeList.Attributes...) + } + + return result +} + +// Selfie is a photograph of the user. Will be nil if not provided by Yoti. +func (p UserProfile) Selfie() *attribute.ImageAttribute { + return p.GetImageAttribute(consts.AttrSelfie) +} + +// GetSelfieAttributeByID retrieve a Selfie attribute by ID on the Yoti profile. +// This attribute is a photograph of the user. +// Will return nil if attribute is not present. +func (p UserProfile) GetSelfieAttributeByID(attributeID string) (*attribute.ImageAttribute, error) { + for _, a := range p.attributeSlice { + if a.EphemeralId == attributeID { + return attribute.NewImage(a) + } + } + return nil, nil +} + +// GivenNames corresponds to secondary names in passport, and first/middle names in English. Will be nil if not provided by Yoti. +func (p UserProfile) GivenNames() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrGivenNames) +} + +// FamilyName corresponds to primary name in passport, and surname in English. Will be nil if not provided by Yoti. +func (p UserProfile) FamilyName() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrFamilyName) +} + +// FullName represents the user's full name. +// If family_name/given_names are present, the value will be equal to the string 'given_names + " " family_name'. +// Will be nil if not provided by Yoti. +func (p UserProfile) FullName() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrFullName) +} + +// MobileNumber represents the user's mobile phone number, as verified at registration time. +// The value will be a number in E.164 format (i.e. '+' for international prefix and no spaces, e.g. "+447777123456"). +// Will be nil if not provided by Yoti. +func (p UserProfile) MobileNumber() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrMobileNumber) +} + +// EmailAddress represents the user's verified email address. Will be nil if not provided by Yoti. +func (p UserProfile) EmailAddress() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrEmailAddress) +} + +// DateOfBirth represents the user's date of birth. Will be nil if not provided by Yoti. +// Has an err value which will be filled if there is an error parsing the date. +func (p UserProfile) DateOfBirth() (*attribute.DateAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == consts.AttrDateOfBirth { + return attribute.NewDate(a) + } + } + return nil, nil +} + +// Address represents the user's address. Will be nil if not provided by Yoti. +func (p UserProfile) Address() *attribute.StringAttribute { + addressAttribute := p.GetStringAttribute(consts.AttrAddress) + if addressAttribute == nil { + return ensureAddressProfile(&p) + } + + return addressAttribute +} + +// StructuredPostalAddress represents the user's address in a JSON format. +// Will be nil if not provided by Yoti. This can be accessed as a +// map[string]string{} using a type assertion, e.g.: +// structuredPostalAddress := structuredPostalAddressAttribute.Value().(map[string]string{}) +func (p UserProfile) StructuredPostalAddress() (*attribute.JSONAttribute, error) { + return p.GetJSONAttribute(consts.AttrStructuredPostalAddress) +} + +// Gender corresponds to the gender in the registered document; the value will be one of the strings "MALE", "FEMALE", "TRANSGENDER" or "OTHER". +// Will be nil if not provided by Yoti. +func (p UserProfile) Gender() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrGender) +} + +// Nationality corresponds to the nationality in the passport. +// The value is an ISO-3166-1 alpha-3 code with ICAO9303 (passport) extensions. +// Will be nil if not provided by Yoti. +func (p UserProfile) Nationality() *attribute.StringAttribute { + return p.GetStringAttribute(consts.AttrNationality) +} + +// DocumentImages returns a slice of document images cropped from the image in the capture page. +// There can be multiple images as per the number of regions in the capture in this attribute. +// Will be nil if not provided by Yoti. +func (p UserProfile) DocumentImages() (*attribute.ImageSliceAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == consts.AttrDocumentImages { + return attribute.NewImageSlice(a) + } + } + return nil, nil +} + +// GetDocumentImagesAttributeByID retrieve a Document Images attribute by ID on the Yoti profile. +// This attribute consists of a slice of document images cropped from the image in the capture page. +// There can be multiple images as per the number of regions in the capture in this attribute. +// Will return nil if attribute is not present. +func (p UserProfile) GetDocumentImagesAttributeByID(attributeID string) (*attribute.ImageSliceAttribute, error) { + for _, a := range p.attributeSlice { + if a.EphemeralId == attributeID { + return attribute.NewImageSlice(a) + } + } + return nil, nil +} + +// DocumentDetails represents information extracted from a document provided by the user. +// Will be nil if not provided by Yoti. +func (p UserProfile) DocumentDetails() (*attribute.DocumentDetailsAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == consts.AttrDocumentDetails { + return attribute.NewDocumentDetails(a) + } + } + return nil, nil +} + +// IdentityProfileReport represents the JSON object containing identity assertion and the +// verification report. Will be nil if not provided by Yoti. +func (p UserProfile) IdentityProfileReport() (*attribute.JSONAttribute, error) { + return p.GetJSONAttribute(consts.AttrIdentityProfileReport) +} + +// AgeVerifications returns a slice of age verifications for the user. +// Will be an empty slice if not provided by Yoti. +func (p UserProfile) AgeVerifications() (out []attribute.AgeVerification, err error) { + ageUnderString := strings.Replace(consts.AttrAgeUnder, "%d", "", -1) + ageOverString := strings.Replace(consts.AttrAgeOver, "%d", "", -1) + + for _, a := range p.attributeSlice { + if strings.HasPrefix(a.Name, ageUnderString) || + strings.HasPrefix(a.Name, ageOverString) { + verification, err := attribute.NewAgeVerification(a) + if err != nil { + return nil, err + } + out = append(out, verification) + } + } + return out, err +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/user_profile_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/user_profile_test.go new file mode 100644 index 0000000..0750fa6 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/profile/user_profile_test.go @@ -0,0 +1,704 @@ +package profile + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/consts" + "github.com/getyoti/yoti-go-sdk/v3/file" + "github.com/getyoti/yoti-go-sdk/v3/media" + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoattr" + "google.golang.org/protobuf/proto" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +const ( + attributeName = "test_attribute_name" + attributeValueString = "value" + + documentImagesAttributeID = "document-images-attribute-id-123" + selfieAttributeID = "selfie-attribute-id-123" + fullNameAttributeID = "full-name-id-123" +) + +var attributeValue = []byte(attributeValueString) + +func getUserProfile() UserProfile { + userProfile := createProfileWithMultipleAttributes( + createDocumentImagesAttribute(documentImagesAttributeID), + createSelfieAttribute(yotiprotoattr.ContentType_JPEG, selfieAttributeID), + createStringAttribute("full_name", []byte("John Smith"), []*yotiprotoattr.Anchor{}, fullNameAttributeID)) + + return userProfile +} + +func ExampleUserProfile_GetAttributeByID() { + userProfile := getUserProfile() + fullNameAttribute := userProfile.GetAttributeByID("full-name-id-123") + value := fullNameAttribute.Value().(string) + + fmt.Println(value) + // Output: John Smith +} + +func ExampleUserProfile_GetDocumentImagesAttributeByID() { + userProfile := getUserProfile() + documentImagesAttribute, err := userProfile.GetDocumentImagesAttributeByID("document-images-attribute-id-123") + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(*documentImagesAttribute.ID()) + // Output: document-images-attribute-id-123 +} + +func ExampleUserProfile_GetSelfieAttributeByID() { + userProfile := getUserProfile() + selfieAttribute, err := userProfile.GetSelfieAttributeByID("selfie-attribute-id-123") + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(*selfieAttribute.ID()) + // Output: selfie-attribute-id-123 +} + +func createProfileWithSingleAttribute(attr *yotiprotoattr.Attribute) UserProfile { + var attributeSlice []*yotiprotoattr.Attribute + attributeSlice = append(attributeSlice, attr) + + return UserProfile{ + baseProfile{ + attributeSlice: attributeSlice, + }, + } +} + +func createAppProfileWithSingleAttribute(attr *yotiprotoattr.Attribute) ApplicationProfile { + var attributeSlice []*yotiprotoattr.Attribute + attributeSlice = append(attributeSlice, attr) + + return ApplicationProfile{ + baseProfile{ + attributeSlice: attributeSlice, + }, + } +} + +func createProfileWithMultipleAttributes(list ...*yotiprotoattr.Attribute) UserProfile { + return UserProfile{ + baseProfile{ + attributeSlice: list, + }, + } +} + +func TestProfile_AgeVerifications(t *testing.T) { + ageOver14 := &yotiprotoattr.Attribute{ + Name: "age_over:14", + Value: []byte("true"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + ageUnder18 := &yotiprotoattr.Attribute{ + Name: "age_under:18", + Value: []byte("true"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + ageOver18 := &yotiprotoattr.Attribute{ + Name: "age_over:18", + Value: []byte("false"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithMultipleAttributes(ageOver14, ageUnder18, ageOver18) + ageVerifications, err := profile.AgeVerifications() + + assert.NilError(t, err) + assert.Equal(t, len(ageVerifications), 3) + + assert.Equal(t, ageVerifications[0].Age, 14) + assert.Equal(t, ageVerifications[0].CheckType, "age_over") + assert.Equal(t, ageVerifications[0].Result, true) + + assert.Equal(t, ageVerifications[1].Age, 18) + assert.Equal(t, ageVerifications[1].CheckType, "age_under") + assert.Equal(t, ageVerifications[1].Result, true) + + assert.Equal(t, ageVerifications[2].Age, 18) + assert.Equal(t, ageVerifications[2].CheckType, "age_over") + assert.Equal(t, ageVerifications[2].Result, false) +} + +func TestProfile_GetAttribute_EmptyString(t *testing.T) { + emptyString := "" + attributeValue := []byte(emptyString) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + assert.Equal(t, att.Name(), attributeName) + assert.Equal(t, att.Value().(string), emptyString) +} + +func TestProfile_GetApplicationAttribute(t *testing.T) { + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createProfileWithSingleAttribute(attr) + applicationAttribute := appProfile.GetAttribute(attributeName) + assert.Equal(t, applicationAttribute.Name(), attributeName) +} + +func TestProfile_GetApplicationName(t *testing.T) { + attributeValue := "APPLICATION NAME" + var attr = &yotiprotoattr.Attribute{ + Name: "application_name", + Value: []byte(attributeValue), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createAppProfileWithSingleAttribute(attr) + assert.Equal(t, attributeValue, appProfile.ApplicationName().Value()) +} + +func TestProfile_GetApplicationURL(t *testing.T) { + attributeValue := "APPLICATION URL" + var attr = &yotiprotoattr.Attribute{ + Name: "application_url", + Value: []byte(attributeValue), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createAppProfileWithSingleAttribute(attr) + assert.Equal(t, attributeValue, appProfile.ApplicationURL().Value()) +} + +func TestProfile_GetApplicationLogo(t *testing.T) { + attributeValue := "APPLICATION LOGO" + var attr = &yotiprotoattr.Attribute{ + Name: "application_logo", + Value: []byte(attributeValue), + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createAppProfileWithSingleAttribute(attr) + assert.Equal(t, 16, len(appProfile.ApplicationLogo().Value().Data())) +} + +func TestProfile_GetApplicationBGColor(t *testing.T) { + attributeValue := "BG VALUE" + var attr = &yotiprotoattr.Attribute{ + Name: "application_receipt_bgcolor", + Value: []byte(attributeValue), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + appProfile := createAppProfileWithSingleAttribute(attr) + assert.Equal(t, attributeValue, appProfile.ApplicationReceiptBgColor().Value()) +} + +func TestProfile_GetAttribute_Int(t *testing.T) { + intValues := [5]int{0, 1, 123, -10, -1} + + for _, integer := range intValues { + assertExpectedIntegerIsReturned(t, integer) + } +} + +func assertExpectedIntegerIsReturned(t *testing.T, intValue int) { + intAsString := strconv.Itoa(intValue) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: []byte(intAsString), + ContentType: yotiprotoattr.ContentType_INT, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + assert.Equal(t, att.Value().(int), intValue) +} + +func TestProfile_GetAttribute_InvalidInt_ReturnsNil(t *testing.T) { + invalidIntValue := "1985-01-01" + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: []byte(invalidIntValue), + ContentType: yotiprotoattr.ContentType_INT, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + + att := result.GetAttribute(attributeName) + + assert.Assert(t, is.Nil(att)) +} + +func TestProfile_EmptyStringIsAllowed(t *testing.T) { + emptyString := "" + attrValue := []byte(emptyString) + + var attr = &yotiprotoattr.Attribute{ + Name: consts.AttrGender, + Value: attrValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(attr) + att := profile.Gender() + + assert.Equal(t, att.Value(), emptyString) +} + +func TestProfile_GetAttribute_Time(t *testing.T) { + dateStringValue := "1985-01-01" + expectedDate := time.Date(1985, time.January, 1, 0, 0, 0, 0, time.UTC) + + attributeValueTime := []byte(dateStringValue) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValueTime, + ContentType: yotiprotoattr.ContentType_DATE, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + assert.Equal(t, expectedDate, att.Value().(*time.Time).UTC()) +} + +func TestProfile_GetAttribute_Jpeg(t *testing.T) { + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(attr) + att := profile.GetAttribute(attributeName) + + expected := media.JPEGImage(attributeValue) + result := att.Value().(media.JPEGImage) + + assert.DeepEqual(t, expected, result) + assert.Equal(t, expected.Base64URL(), result.Base64URL()) +} + +func TestProfile_GetAttribute_Png(t *testing.T) { + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(attr) + att := profile.GetAttribute(attributeName) + + expected := media.PNGImage(attributeValue) + result := att.Value().(media.PNGImage) + + assert.DeepEqual(t, expected, result) + assert.Equal(t, expected.Base64URL(), result.Base64URL()) +} + +func TestProfile_GetAttribute_Bool(t *testing.T) { + var initialBoolValue = true + attrValue := []byte(strconv.FormatBool(initialBoolValue)) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attrValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + boolValue, err := strconv.ParseBool(att.Value().(string)) + + assert.NilError(t, err) + assert.Equal(t, initialBoolValue, boolValue) +} + +func TestProfile_GetAttribute_JSON(t *testing.T) { + addressFormat := "2" + + var structuredAddressBytes = []byte(` + { + "address_format": "` + addressFormat + `", + "building": "House No.86-A" + }`) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: structuredAddressBytes, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + retrievedAttributeMap := att.Value().(map[string]interface{}) + actualAddressFormat := retrievedAttributeMap["address_format"] + + assert.Equal(t, actualAddressFormat, addressFormat) +} + +func TestProfile_GetAttribute_Undefined(t *testing.T) { + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + assert.Equal(t, att.Name(), attributeName) + assert.Equal(t, att.Value().(string), attributeValueString) +} + +func TestProfile_GetAttribute_ReturnsNil(t *testing.T) { + userProfile := UserProfile{ + baseProfile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + }, + } + + result := userProfile.GetAttribute("attributeName") + + assert.Assert(t, is.Nil(result)) +} + +func TestProfile_GetAttributeByID(t *testing.T) { + attributeID := "att-id-123" + + var attr1 = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + EphemeralId: attributeID, + } + var attr2 = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + EphemeralId: "non-matching-attribute-ID", + } + + profile := createProfileWithMultipleAttributes(attr1, attr2) + + result := profile.GetAttributeByID(attributeID) + assert.DeepEqual(t, result.ID(), &attributeID) +} + +func TestProfile_GetAttributeByID_ReturnsNil(t *testing.T) { + userProfile := UserProfile{ + baseProfile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + }, + } + + result := userProfile.GetAttributeByID("attributeName") + + assert.Assert(t, is.Nil(result)) +} + +func TestProfile_GetDocumentImagesAttributeByID_ReturnsNil(t *testing.T) { + userProfile := UserProfile{ + baseProfile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + }, + } + + result, err := userProfile.GetDocumentImagesAttributeByID("attributeName") + assert.NilError(t, err) + assert.Assert(t, is.Nil(result)) +} + +func TestProfile_GetSelfieAttributeByID_ReturnsNil(t *testing.T) { + userProfile := UserProfile{ + baseProfile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + }, + } + + result, err := userProfile.GetSelfieAttributeByID("attributeName") + assert.NilError(t, err) + assert.Assert(t, is.Nil(result)) +} + +func TestProfile_StringAttribute(t *testing.T) { + nationalityName := consts.AttrNationality + + var as = &yotiprotoattr.Attribute{ + Name: nationalityName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(as) + + assert.Equal(t, result.Nationality().Value(), attributeValueString) + + assert.Equal(t, result.Nationality().ContentType(), yotiprotoattr.ContentType_STRING.String()) +} + +func TestProfile_AttributeProperty_RetrievesAttribute(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + assert.Equal(t, selfie.Name(), consts.AttrSelfie) + assert.DeepEqual(t, attributeValue, selfie.Value().Data()) + assert.Equal(t, selfie.ContentType(), yotiprotoattr.ContentType_PNG.String()) +} + +func TestProfile_DocumentDetails_RetrievesAttribute(t *testing.T) { + documentDetailsName := consts.AttrDocumentDetails + attributeValue := []byte("PASSPORT GBR 1234567") + + var protoAttribute = &yotiprotoattr.Attribute{ + Name: documentDetailsName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: make([]*yotiprotoattr.Anchor, 0), + } + + result := createProfileWithSingleAttribute(protoAttribute) + documentDetails, err := result.DocumentDetails() + assert.NilError(t, err) + + assert.Equal(t, documentDetails.Value().DocumentType, "PASSPORT") +} + +func TestProfile_DocumentImages_RetrievesAttribute(t *testing.T) { + protoAttribute := createDocumentImagesAttribute("attr-id") + + result := createProfileWithSingleAttribute(protoAttribute) + documentImages, err := result.DocumentImages() + assert.NilError(t, err) + + assert.Equal(t, documentImages.Name(), consts.AttrDocumentImages) +} + +func TestProfile_AttributesReturnsNilWhenNotPresent(t *testing.T) { + documentImagesName := consts.AttrDocumentImages + multiValue, err := proto.Marshal(&yotiprotoattr.MultiValue{}) + assert.NilError(t, err) + + protoAttribute := &yotiprotoattr.Attribute{ + Name: documentImagesName, + Value: multiValue, + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Anchors: make([]*yotiprotoattr.Anchor, 0), + } + + result := createProfileWithSingleAttribute(protoAttribute) + + DoB, err := result.DateOfBirth() + assert.Check(t, DoB == nil) + assert.Check(t, err == nil) + assert.Check(t, result.Address() == nil) +} + +func TestMissingPostalAddress_UsesFormattedAddress(t *testing.T) { + var formattedAddressText = `House No.86-A\nRajgura Nagar\nLudhina\nPunjab\n141012\nIndia` + + var structuredAddressBytes = []byte(` + { + "address_format": 2, + "building": "House No.86-A", + "formatted_address": "` + formattedAddressText + `" + } + `) + + var jsonAttribute = &yotiprotoattr.Attribute{ + Name: consts.AttrStructuredPostalAddress, + Value: structuredAddressBytes, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(jsonAttribute) + + ensureAddressProfile(&profile) + + escapedFormattedAddressText := strings.Replace(formattedAddressText, `\n`, "\n", -1) + + profileAddress := profile.Address().Value() + assert.Equal(t, profileAddress, escapedFormattedAddressText, "Address does not equal the expected formatted address.") + + structuredPostalAddress, err := profile.StructuredPostalAddress() + assert.NilError(t, err) + assert.Equal(t, structuredPostalAddress.ContentType(), "JSON") +} + +func TestAttributeImage_Image_Png(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + assert.DeepEqual(t, selfie.Value().Data(), attributeValue) +} + +func TestAttributeImage_Image_Jpeg(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_JPEG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + assert.DeepEqual(t, selfie.Value().Data(), attributeValue) +} + +func TestAttributeImage_Image_Default(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + assert.DeepEqual(t, selfie.Value().Data(), attributeValue) +} +func TestAttributeImage_Base64Selfie_Png(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_PNG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(attributeValue) + expectedBase64Selfie := "data:image/png;base64," + base64ImageExpectedValue + base64Selfie := result.Selfie().Value().Base64URL() + + assert.Equal(t, base64Selfie, expectedBase64Selfie) +} + +func TestAttributeImage_Base64URL_Jpeg(t *testing.T) { + attributeImage := createSelfieAttribute(yotiprotoattr.ContentType_JPEG, "id") + + result := createProfileWithSingleAttribute(attributeImage) + + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(attributeValue) + + expectedBase64Selfie := "data:image/jpeg;base64," + base64ImageExpectedValue + + base64Selfie := result.Selfie().Value().Base64URL() + + assert.Equal(t, base64Selfie, expectedBase64Selfie) +} + +func TestProfile_IdentityProfileReport_RetrievesAttribute(t *testing.T) { + identityProfileReportJSON, err := file.ReadFile("../test/fixtures/RTWIdentityProfileReport.json") + assert.NilError(t, err) + + var attr = &yotiprotoattr.Attribute{ + Name: consts.AttrIdentityProfileReport, + Value: identityProfileReportJSON, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att, err := result.IdentityProfileReport() + assert.NilError(t, err) + + retrievedIdentityProfile := att.Value() + gotProof := retrievedIdentityProfile["proof"] + + assert.Equal(t, gotProof, "") +} + +func TestProfileAllowsMultipleAttributesWithSameName(t *testing.T) { + firstAttribute := createStringAttribute("full_name", []byte("some_value"), []*yotiprotoattr.Anchor{}, "id") + secondAttribute := createStringAttribute("full_name", []byte("some_other_value"), []*yotiprotoattr.Anchor{}, "id") + + var attributeSlice []*yotiprotoattr.Attribute + attributeSlice = append(attributeSlice, firstAttribute, secondAttribute) + + var profile = UserProfile{ + baseProfile{ + attributeSlice: attributeSlice, + }, + } + + var fullNames = profile.GetAttributes("full_name") + + assert.Assert(t, is.Equal(len(fullNames), 2)) + assert.Assert(t, is.Equal(fullNames[0].Value().(string), "some_value")) + assert.Assert(t, is.Equal(fullNames[1].Value().(string), "some_other_value")) +} + +func createStringAttribute(name string, value []byte, anchors []*yotiprotoattr.Anchor, attributeID string) *yotiprotoattr.Attribute { + return &yotiprotoattr.Attribute{ + Name: name, + Value: value, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: anchors, + EphemeralId: attributeID, + } +} + +func createSelfieAttribute(contentType yotiprotoattr.ContentType, attributeID string) *yotiprotoattr.Attribute { + var attributeImage = &yotiprotoattr.Attribute{ + Name: consts.AttrSelfie, + Value: attributeValue, + ContentType: contentType, + Anchors: []*yotiprotoattr.Anchor{}, + EphemeralId: attributeID, + } + return attributeImage +} + +func createDocumentImagesAttribute(attributeID string) *yotiprotoattr.Attribute { + multiValue, err := proto.Marshal(&yotiprotoattr.MultiValue{}) + if err != nil { + panic(err) + } + + protoAttribute := &yotiprotoattr.Attribute{ + Name: consts.AttrDocumentImages, + Value: multiValue, + ContentType: yotiprotoattr.ContentType_MULTI_VALUE, + Anchors: make([]*yotiprotoattr.Anchor, 0), + EphemeralId: attributeID, + } + return protoAttribute +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/client.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/client.go new file mode 100644 index 0000000..74c289e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/client.go @@ -0,0 +1,10 @@ +package requests + +import ( + "net/http" +) + +// HttpClient is a mockable HTTP Client Interface +type HttpClient interface { + Do(*http.Request) (*http.Response, error) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/request.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/request.go new file mode 100644 index 0000000..b1fb602 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/request.go @@ -0,0 +1,38 @@ +package requests + +import ( + "net/http" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotierror" +) + +// Execute makes a request to the specified endpoint, with an optional payload +func Execute(httpClient HttpClient, request *http.Request, httpErrorMessages ...map[int]string) (response *http.Response, err error) { + if response, err = doRequest(request, httpClient); err != nil { + return + } + + statusCodeIsFailure := response.StatusCode >= 300 || response.StatusCode < 200 + + if statusCodeIsFailure { + return response, yotierror.NewResponseError(response, httpErrorMessages...) + } + + return response, nil +} + +func doRequest(request *http.Request, httpClient HttpClient) (*http.Response, error) { + httpClient = ensureHttpClientTimeout(httpClient) + return httpClient.Do(request) +} + +func ensureHttpClientTimeout(httpClient HttpClient) HttpClient { + if httpClient == nil { + httpClient = &http.Client{ + Timeout: time.Second * 10, + } + } + + return httpClient +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/request_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/request_test.go new file mode 100644 index 0000000..0137f2f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/request_test.go @@ -0,0 +1,90 @@ +package requests + +import ( + "errors" + "net/http" + "testing" + "time" + + "gotest.tools/v3/assert" +) + +type mockHTTPClient struct { + do func(*http.Request) (*http.Response, error) +} + +func (mock *mockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if mock.do != nil { + return mock.do(request) + } + return nil, nil +} + +func TestExecute_Success(t *testing.T) { + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + }, nil + }, + } + + request := &http.Request{ + Method: http.MethodGet, + } + + response, err := Execute(client, request) + + assert.NilError(t, err) + assert.Equal(t, response.StatusCode, 200) +} + +func TestExecute_Failure(t *testing.T) { + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 400, + }, nil + }, + } + + request := &http.Request{ + Method: http.MethodGet, + } + + response, err := Execute(client, request) + + assert.ErrorContains(t, err, "400: unknown HTTP error") + assert.Equal(t, response.StatusCode, 400) +} + +func TestExecute_ClientError(t *testing.T) { + client := &mockHTTPClient{ + do: func(*http.Request) (*http.Response, error) { + return nil, errors.New("some error") + }, + } + + request := &http.Request{ + Method: http.MethodGet, + } + + _, err := Execute(client, request) + + assert.ErrorContains(t, err, "some error") +} + +func TestEnsureHttpClientTimeout_NilHTTPClientShouldUse10sTimeout(t *testing.T) { + result := ensureHttpClientTimeout(nil).(*http.Client) + + assert.Equal(t, 10*time.Second, result.Timeout) +} + +func TestEnsureHttpClientTimeout(t *testing.T) { + httpClient := &http.Client{ + Timeout: time.Minute * 12, + } + result := ensureHttpClientTimeout(httpClient).(*http.Client) + + assert.Equal(t, 12*time.Minute, result.Timeout) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/signed_message.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/signed_message.go new file mode 100644 index 0000000..e90fc54 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/signed_message.go @@ -0,0 +1,215 @@ +package requests + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/consts" +) + +// MergeHeaders merges two or more header prototypes together from left to right +func MergeHeaders(headers ...map[string][]string) map[string][]string { + if len(headers) == 0 { + return make(map[string][]string) + } + out := headers[0] + for _, element := range headers[1:] { + for k, v := range element { + out[k] = v + } + } + return out +} + +// JSONHeaders is a header prototype for JSON based requests +func JSONHeaders() map[string][]string { + return map[string][]string{ + "Content-Type": {"application/json"}, + "Accept": {"application/json"}, + } +} + +// AuthKeyHeader is a header prototype including an encoded RSA PublicKey +func AuthKeyHeader(key *rsa.PublicKey) map[string][]string { + return map[string][]string{ + "X-Yoti-Auth-Key": { + base64.StdEncoding.EncodeToString( + func(a []byte, _ error) []byte { + return a + }(x509.MarshalPKIXPublicKey(key)), + ), + }, + } +} + +// SignedRequest is a builder for constructing a http.Request with Yoti signing +type SignedRequest struct { + Key *rsa.PrivateKey + HTTPMethod string + BaseURL string + Endpoint string + Headers map[string][]string + Params map[string]string + Body []byte + Error error +} + +func (msg *SignedRequest) signDigest(digest []byte) (string, error) { + hash := sha256.Sum256(digest) + signed, err := rsa.SignPKCS1v15(rand.Reader, msg.Key, crypto.SHA256, hash[:]) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(signed), nil +} + +func getTimestamp() string { + return strconv.FormatInt(time.Now().Unix()*1000, 10) +} + +func getNonce() (string, error) { + nonce := make([]byte, 16) + _, err := rand.Read(nonce) + return fmt.Sprintf("%X-%X-%X-%X-%X", nonce[0:4], nonce[4:6], nonce[6:8], nonce[8:10], nonce[10:]), err +} + +// WithPemFile loads the private key from a PEM file reader +func (msg SignedRequest) WithPemFile(in []byte) SignedRequest { + block, _ := pem.Decode(in) + if block == nil { + msg.Error = errors.New("input is not PEM-encoded") + return msg + } + if block.Type != "RSA PRIVATE KEY" { + msg.Error = errors.New("input is not an RSA Private Key") + return msg + } + + msg.Key, msg.Error = x509.ParsePKCS1PrivateKey(block.Bytes) + return msg +} + +func (msg *SignedRequest) addParametersToEndpoint() (string, error) { + if msg.Params == nil { + msg.Params = make(map[string]string) + } + // Add Timestamp/Nonce + if _, ok := msg.Params["nonce"]; !ok { + nonce, err := getNonce() + if err != nil { + return "", err + } + msg.Params["nonce"] = nonce + } + if _, ok := msg.Params["timestamp"]; !ok { + msg.Params["timestamp"] = getTimestamp() + } + + endpoint := msg.Endpoint + if !strings.Contains(endpoint, "?") { + endpoint = endpoint + "?" + } else { + endpoint = endpoint + "&" + } + + var firstParam = true + for param, value := range msg.Params { + var formatString = "%s&%s=%s" + if firstParam { + formatString = "%s%s=%s" + } + endpoint = fmt.Sprintf(formatString, endpoint, param, value) + firstParam = false + } + + return endpoint, nil +} + +func (msg *SignedRequest) generateDigest(endpoint string) (digest string) { + // Generate the message digest + if msg.Body != nil { + digest = fmt.Sprintf( + "%s&%s&%s", + msg.HTTPMethod, + endpoint, + base64.StdEncoding.EncodeToString(msg.Body), + ) + } else { + digest = fmt.Sprintf("%s&%s", + msg.HTTPMethod, + endpoint, + ) + } + return +} + +func (msg *SignedRequest) checkMandatories() error { + if msg.Error != nil { + return msg.Error + } + if msg.Key == nil { + return fmt.Errorf("missing private key") + } + if msg.HTTPMethod == "" { + return fmt.Errorf("missing HTTPMethod") + } + if msg.BaseURL == "" { + return fmt.Errorf("missing BaseURL") + } + if msg.Endpoint == "" { + return fmt.Errorf("missing Endpoint") + } + return nil +} + +// Request builds a http.Request with signature headers +func (msg SignedRequest) Request() (request *http.Request, err error) { + err = msg.checkMandatories() + if err != nil { + return + } + + endpoint, err := msg.addParametersToEndpoint() + if err != nil { + return + } + + signedDigest, err := msg.signDigest([]byte(msg.generateDigest(endpoint))) + if err != nil { + return + } + + // Construct the HTTP Request + request, err = http.NewRequest( + msg.HTTPMethod, + msg.BaseURL+endpoint, + bytes.NewReader(msg.Body), + ) + if err != nil { + return + } + + request.Header.Add("X-Yoti-Auth-Digest", signedDigest) + request.Header.Add("X-Yoti-SDK", consts.SDKIdentifier) + request.Header.Add("X-Yoti-SDK-Version", consts.SDKVersionIdentifier) + + for key, values := range msg.Headers { + for _, value := range values { + request.Header.Add(key, value) + } + } + + return request, err +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/signed_message_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/signed_message_test.go new file mode 100644 index 0000000..1e23205 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/requests/signed_message_test.go @@ -0,0 +1,169 @@ +package requests + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "regexp" + "testing" + + "gotest.tools/v3/assert" +) + +const exampleKey = "MIICXgIBAAKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQABAoGBAIJL7GbSvjZUVVU1E6TZd0+9lhqmGf/S2o5309bxSfQ/oxxSyrHU9nMNTqcjCZXuJCTKS7hOKmXY5mbOYvvZ0xA7DXfOc+A4LGXQl0r3ZMzhHZTPKboUSh16E4WI4pr98KagFdkeB/0KBURM3x5d/6dSKip8ZpEyqVpuc9d1xtvhAkEAxabfsqfb4fgBsrhZ/qt133yB0FBHs1alRxvUXZWbVPTOegKi5KBdPptf2QfCy8WK3An/lg8cFQG78PyNll/P0QJBANtJBUHTuRDCoYLhqZLdSTQ52qOWRNutZ2fho9ZcLquokB4SFFeC2I4T+s3oSJ8SNh9vW1nNeXW6Zipx+zz8O58CQQCjV9qNGf40zDITEhmFxwt967aYgpAO3O9wScaCpM4fMsWkvaMDEKiewec/RBOvNY0hdb3ctJX/olRAv2b/vCTRAkAuLmCnDlnJR9QP5kp6HZRPJWgAT6NMyGYgoIqKmHtTt3oyewhBrdLBiT+moaa5qXIwiJkqfnV377uYcMzCeTRtAkEAwHdhM3v01GprmHqE2kvlKOXNq9CB1Z4j/vXSQxBYoSrFWLv5nW9e69ngX+n7qhvO3Gs9CBoy/oqOLatFZOuFEw==" + +var keyBytes, _ = base64.StdEncoding.DecodeString(exampleKey) +var privateKey, _ = x509.ParsePKCS1PrivateKey(keyBytes) + +func ExampleMergeHeaders() { + left := map[string][]string{"A": {"Value Of A"}} + right := map[string][]string{"B": {"Value Of B"}} + + merged := MergeHeaders(left, right) + fmt.Println(merged["A"]) + fmt.Println(merged["B"]) + // Output: + // [Value Of A] + // [Value Of B] +} + +func TestMergeHeaders_HandleNullCaseGracefully(t *testing.T) { + assert.Equal(t, len(MergeHeaders()), 0) +} + +func ExampleJSONHeaders() { + jsonHeaders, err := json.Marshal(JSONHeaders()) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(jsonHeaders)) + // Output: {"Accept":["application/json"],"Content-Type":["application/json"]} +} + +func ExampleAuthKeyHeader() { + headers, err := json.Marshal(AuthKeyHeader(&privateKey.PublicKey)) + if err != nil { + fmt.Printf("error: %s", err.Error()) + return + } + + fmt.Println(string(headers)) + // Output: {"X-Yoti-Auth-Key":["MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQAB"]} +} + +func TestRequestShouldBuildForValid(t *testing.T) { + random := rand.New(rand.NewSource(25)) + key, err := rsa.GenerateKey(random, 1024) + + assert.NilError(t, err) + httpMethod := "GET" + baseURL := "example.com" + endpoint := "/" + + request := SignedRequest{ + Key: key, + HTTPMethod: httpMethod, + BaseURL: baseURL, + Endpoint: endpoint, + } + signed, err := request.Request() + assert.NilError(t, err) + assert.Equal(t, httpMethod, signed.Method) + urlCheck, err := regexp.Match(baseURL+endpoint, []byte(signed.URL.String())) + assert.NilError(t, err) + assert.Check(t, urlCheck) + assert.Check(t, signed.Header.Get("X-Yoti-Auth-Digest") != "") + assert.Equal(t, signed.Header.Get("X-Yoti-SDK"), "Go") + assert.Equal(t, signed.Header.Get("X-Yoti-SDK-Version"), "3.14.0") +} + +func TestRequestShouldAddHeaders(t *testing.T) { + random := rand.New(rand.NewSource(25)) + key, err := rsa.GenerateKey(random, 1024) + + assert.NilError(t, err) + httpMethod := "GET" + baseURL := "example.com" + endpoint := "/" + + request := SignedRequest{ + Key: key, + HTTPMethod: httpMethod, + BaseURL: baseURL, + Endpoint: endpoint, + Headers: JSONHeaders(), + } + signed, err := request.Request() + assert.NilError(t, err) + assert.Check(t, signed.Header["X-Yoti-Auth-Digest"][0] != "") + assert.Equal(t, signed.Header["Accept"][0], "application/json") +} + +func TestSignedRequest_checkMandatories_WhenErrorIsSetReturnIt(t *testing.T) { + msg := &SignedRequest{Error: fmt.Errorf("exampleError")} + assert.Error(t, msg.checkMandatories(), "exampleError") +} + +func TestSignedRequest_checkMandatories_WhenKeyMissing(t *testing.T) { + msg := &SignedRequest{} + assert.Error(t, msg.checkMandatories(), "missing private key") +} + +func TestSignedRequest_checkMandatories_WhenHTTPMethodMissing(t *testing.T) { + msg := &SignedRequest{Key: privateKey} + assert.Error(t, msg.checkMandatories(), "missing HTTPMethod") +} + +func TestSignedRequest_checkMandatories_WhenBaseURLMissing(t *testing.T) { + msg := &SignedRequest{ + Key: privateKey, + HTTPMethod: http.MethodPost, + } + assert.Error(t, msg.checkMandatories(), "missing BaseURL") +} + +func TestSignedRequest_checkMandatories_WhenEndpointMissing(t *testing.T) { + msg := &SignedRequest{ + Key: privateKey, + HTTPMethod: http.MethodPost, + BaseURL: "example.com", + } + assert.Error(t, msg.checkMandatories(), "missing Endpoint") +} + +func ExampleSignedRequest_generateDigest() { + msg := &SignedRequest{ + HTTPMethod: http.MethodPost, + Body: []byte("simple message body"), + } + fmt.Println(msg.generateDigest("endpoint")) + // Output: POST&endpoint&c2ltcGxlIG1lc3NhZ2UgYm9keQ== + +} + +func ExampleSignedRequest_WithPemFile() { + msg := SignedRequest{}.WithPemFile([]byte(` +-----BEGIN RSA PRIVATE KEY----- +` + exampleKey + ` +-----END RSA PRIVATE KEY-----`)) + fmt.Println(AuthKeyHeader(&msg.Key.PublicKey)) + // Output: map[X-Yoti-Auth-Key:[MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpTiICtL+ujx8D0FquVWIaXg+ajJadN5hsTlGUXymiFAunSZjLjTsoGfSPz8PJm6pG9ax1Qb+R5UsSgTRTcpZTps2RLRWr5oPfD66bz4l38QXPSvfg5o+5kNxyCb8QANitF7Ht/DcpsGpL7anruHg/RgCLCBFRaGAodfuJCCM9zwIDAQAB]] +} + +func TestSignedRequest_WithPemFile_NotPemEncodedShouldError(t *testing.T) { + msg := SignedRequest{}.WithPemFile([]byte("not pem encoded")) + assert.ErrorContains(t, msg.Error, "not PEM-encoded") +} + +func TestSignedRequest_WithPemFile_NotRSAKeyShouldError(t *testing.T) { + msg := SignedRequest{}.WithPemFile([]byte(`-----BEGIN RSA PUBLIC KEY----- +` + exampleKey + ` +-----END RSA PUBLIC KEY-----`)) + assert.ErrorContains(t, msg.Error, "not an RSA Private Key") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/go-build-modtidy.sh b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/go-build-modtidy.sh new file mode 100644 index 0000000..bef8763 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/go-build-modtidy.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +go build ./... + +for d in _examples/*/; do + (cd "$d" && go mod tidy -compat=1.19) +done diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/gofmt.sh b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/gofmt.sh new file mode 100644 index 0000000..e7cee65 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/gofmt.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +unset dirs files +dirs=$(go list -f {{.Dir}} ./... | grep -v /yotiprotoshare/ | grep -v /yotiprotocom/ | grep -v /yotiprotoattr/) +for d in $dirs; do + for f in $d/*.go; do + files="${files} $f" + done +done +if [ -n "$(gofmt -d $files)" ]; then + echo "Go code is not formatted:" + gofmt -d . + exit 1 +fi diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/goimports.sh b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/goimports.sh new file mode 100644 index 0000000..45e2c24 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/goimports.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +for FILE in $@; do + echo $FILE | if grep --quiet *.go; then + goimports -w $FILE + fi +done diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/test.sh b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/test.sh new file mode 100644 index 0000000..0bebfb8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sh/test.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +set -e +go test -race ./... diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sonar-project.properties b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sonar-project.properties new file mode 100644 index 0000000..b0407bb --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/sonar-project.properties @@ -0,0 +1,15 @@ +sonar.organization = getyoti +sonar.projectKey = getyoti:go +sonar.projectName = Go SDK +sonar.projectVersion = 3.14.0 +sonar.exclusions = **/yotiprotoattr/*.go,**/yotiprotocom/*.go,**/yotiprotoshare/*.go,**/**_test.go,_examples/**/* +sonar.links.scm = https://github.com/getyoti/yoti-go-sdk +sonar.host.url = https://sonarcloud.io + +sonar.sources = . +sonar.go.coverage.reportPaths = coverage.out +sonar.go.tests.reportPaths = sonar-report.json +sonar.tests = . +sonar.test.inclusions = **/*_test.go +sonar.coverage.exclusions = test/**/*,_examples/**/* +sonar.cpd.exclusions = digitalidentity/** diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/attribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/attribute.go new file mode 100644 index 0000000..1e3787e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/attribute.go @@ -0,0 +1,41 @@ +package test + +import ( + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/v3/yotiprotoshare" + "google.golang.org/protobuf/proto" + + "gotest.tools/v3/assert" +) + +// CreateThirdPartyAttributeDataEntry creates a data entry of type "THIRD_PARTY_ATTRIBUTE", with the specified IssuingAttribute details. +func CreateThirdPartyAttributeDataEntry(t *testing.T, expiryDate *time.Time, stringDefinitions []string, tokenValue string) yotiprotoshare.DataEntry { + var protoDefinitions []*yotiprotoshare.Definition + + for _, definition := range stringDefinitions { + protoDefinition := &yotiprotoshare.Definition{ + Name: definition, + } + + protoDefinitions = append(protoDefinitions, protoDefinition) + } + + thirdPartyAttribute := &yotiprotoshare.ThirdPartyAttribute{ + IssuanceToken: []byte(tokenValue), + IssuingAttributes: &yotiprotoshare.IssuingAttributes{ + ExpiryDate: expiryDate.Format("2006-01-02T15:04:05.000Z"), + Definitions: protoDefinitions, + }, + } + + marshalledThirdPartyAttribute, err := proto.Marshal(thirdPartyAttribute) + + assert.NilError(t, err) + + return yotiprotoshare.DataEntry{ + Type: yotiprotoshare.DataEntry_THIRD_PARTY_ATTRIBUTE, + Value: marshalledThirdPartyAttribute, + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/constants.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/constants.go new file mode 100644 index 0000000..76e8a28 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/constants.go @@ -0,0 +1,7 @@ +package test + +const ( + Token = "NpdmVVGC-28356678-c236-4518-9de4-7a93009ccaf0-c5f92f2a-5539-453e-babc-9b06e1d6b7de" + EncryptedToken = "b6H19bUCJhwh6WqQX_sEHWX9RP-A_ANr1fkApwA4Dp2nJQFAjrF9e6YCXhNBpAIhfHnN0iXubyXxXZMNwNMSQ5VOxkqiytrvPykfKQWHC6ypSbfy0ex8ihndaAXG5FUF-qcU8QaFPMy6iF3x0cxnY0Ij0kZj0Ng2t6oiNafb7AhT-VGXxbFbtZu1QF744PpWMuH0LVyBsAa5N5GJw2AyBrnOh67fWMFDKTJRziP5qCW2k4h5vJfiYr_EOiWKCB1d_zINmUm94ZffGXxcDAkq-KxhN1ZuNhGlJ2fKcFh7KxV0BqlUWPsIEiwS0r9CJ2o1VLbEs2U_hCEXaqseEV7L29EnNIinEPVbL4WR7vkF6zQCbK_cehlk2Qwda-VIATqupRO5grKZN78R9lBitvgilDaoE7JB_VFcPoljGQ48kX0wje1mviX4oJHhuO8GdFITS5LTbojGVQWT7LUNgAUe0W0j-FLHYYck3v84OhWTqads5_jmnnLkp9bdJSRuJF0e8pNdePnn2lgF-GIcyW_0kyGVqeXZrIoxnObLpF-YeUteRBKTkSGFcy7a_V_DLiJMPmH8UXDLOyv8TVt3ppzqpyUrLN2JVMbL5wZ4oriL2INEQKvw_boDJjZDGeRlu5m1y7vGDNBRDo64-uQM9fRUULPw-YkABNwC0DeShswzT00=" + WrappedReceiptKey = "kyHPjq2+Y48cx+9yS/XzmW09jVUylSdhbP+3Q9Tc9p6bCEnyfa8vj38AIu744RzzE+Dc4qkSF21VfzQKtJVILfOXu5xRc7MYa5k3zWhjiesg/gsrv7J4wDyyBpHIJB8TWXnubYMbSYQJjlsfwyxE9kGe0YI08pRo2Tiht0bfR5Z/YrhAk4UBvjp84D+oyug/1mtGhKphA4vgPhQ9/y2wcInYxju7Q6yzOsXGaRUXR38Tn2YmY9OBgjxiTnhoYJFP1X9YJkHeWMW0vxF1RHxgIVrpf7oRzdY1nq28qzRg5+wC7cjRpS2i/CKUAo0oVG4pbpXsaFhaTewStVC7UFtA77JHb3EnF4HcSWMnK5FM7GGkL9MMXQenh11NZHKPWXpux0nLZ6/vwffXZfsiyTIcFL/NajGN8C/hnNBljoQ+B3fzWbjcq5ueUOPwARZ1y38W83UwMynzkud/iEdHLaZIu4qUCRkfSxJg7Dc+O9/BdiffkOn2GyFmNjVeq754DCUypxzMkjYxokedN84nK13OU4afVyC7t5DDxAK/MqAc69NCBRLqMi5f8BMeOZfMcSWPGC9a2Qu8VgG125TuZT4+wIykUhGyj3Bb2/fdPsxwuKFR+E0uqs0ZKvcv1tkNRRtKYBqTacgGK9Yoehg12cyLrITLdjU1fmIDn4/vrhztN5w=" +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/decode_test_file.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/decode_test_file.go new file mode 100644 index 0000000..d1dde76 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/decode_test_file.go @@ -0,0 +1,19 @@ +package test + +import ( + "encoding/base64" + "testing" + + "gotest.tools/v3/assert" +) + +// DecodeTestFile reads a test fixture file +func DecodeTestFile(t *testing.T, filename string) (result []byte) { + base64Bytes := readTestFile(t, filename) + base64String := string(base64Bytes) + filebytes, err := base64.StdEncoding.DecodeString(base64String) + + assert.NilError(t, err) + + return filebytes +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fileparser.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fileparser.go new file mode 100644 index 0000000..86c8318 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fileparser.go @@ -0,0 +1,33 @@ +package test + +import ( + "encoding/base64" + "io/ioutil" + "testing" + + "gotest.tools/v3/assert" +) + +// GetTestFileBytes takes a filepath, decodes it from base64, and returns a byte representation of it +func GetTestFileBytes(t *testing.T, filename string) (result []byte) { + base64Bytes := readTestFile(t, filename) + base64String := string(base64Bytes) + filebytes, err := base64.StdEncoding.DecodeString(base64String) + + assert.NilError(t, err) + + return filebytes +} + +// GetTestFileAsString returns a file as a string +func GetTestFileAsString(t *testing.T, filename string) string { + base64Bytes := readTestFile(t, filename) + return string(base64Bytes) +} + +func readTestFile(t *testing.T, filename string) (result []byte) { + b, err := ioutil.ReadFile(filename) + assert.NilError(t, err) + + return b +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/GetSessionResultWithAdvancedIdentityProfile.json b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/GetSessionResultWithAdvancedIdentityProfile.json new file mode 100644 index 0000000..8cfe688 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/GetSessionResultWithAdvancedIdentityProfile.json @@ -0,0 +1,51 @@ +{ + "session_id": "a1746488-efcc-4c59-bd28-f849dcb933a2", + "client_session_token_ttl": 599, + "user_tracking_id": "user-tracking-id", + "biometric_consent": "2022-03-29T11:39:08.473Z", + "state": "COMPLETED", + "client_session_token": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "advanced_identity_profile": { + "subject_id": "someStringHere", + "result": "DONE", + "failure_reason": { + "reason_code": "MANDATORY_DOCUMENT_COULD_NOT_BE_PROVIDED", + "requirements_not_met_details" : [ + { + "failure_type": "ID_DOCUMENT_AUTHENTICITY", + "document_type" : "PASSPORT", + "document_country_iso_code":"GBR", + "audit_id":"a526df5f-a9c1-4e57-8aa3-919256d8e280", + "details": "INCORRECT_DOCUMENT_TYPE" + } + ] + }, + "identity_profile_report": { + "compliance": [{ + "trust_framework": "UK_TFIDA", + "schemes_compliance": [{ + "scheme": { + "type": "DBS", + "objective": "STANDARD" + }, + "requirements_met": true, + "requirements_not_met_info": "some string here" + }] + }], + "media": { + "id": "c69ff2db-6caf-4e74-8386-037711bbc8d7", + "type": "IMAGE", + "created": "2022-03-29T11:39:24Z", + "last_updated": "2022-03-29T11:39:24Z" + } + } + }, + "advanced_identity_profile_preview": { + "media": { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type": "IMAGE", + "created": "2021-06-11T11:39:24Z", + "last_updated": "2021-06-11T11:39:24Z" + } + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/GetSessionResultWithIdentityProfile.json b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/GetSessionResultWithIdentityProfile.json new file mode 100644 index 0000000..44b16e6 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/GetSessionResultWithIdentityProfile.json @@ -0,0 +1,49 @@ +{ + "session_id": "a1746488-efcc-4c59-bd28-f849dcb933a2", + "client_session_token_ttl": 599, + "user_tracking_id": "user-tracking-id", + "biometric_consent": "2022-03-29T11:39:08.473Z", + "state": "COMPLETED", + "client_session_token": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "identity_profile": { + "subject_id": "someStringHere", + "result": "DONE", + "failure_reason": { + "reason_code": "MANDATORY_DOCUMENT_COULD_NOT_BE_PROVIDED", + "requirements_not_met_details" : [ + { + "failure_type": "ID_DOCUMENT_AUTHENTICITY", + "document_type" : "PASSPORT", + "document_country_iso_code":"GBR", + "audit_id":"a526df5f-a9c1-4e57-8aa3-919256d8e280", + "details": "INCORRECT_DOCUMENT_TYPE" + } + ] + }, + "identity_profile_report": { + "trust_framework": "UK_TFIDA", + "schemes_compliance": [{ + "scheme": { + "type": "DBS", + "objective": "STANDARD" + }, + "requirements_met": true, + "requirements_not_met_info": "some string here" + }], + "media": { + "id": "c69ff2db-6caf-4e74-8386-037711bbc8d7", + "type": "IMAGE", + "created": "2022-03-29T11:39:24Z", + "last_updated": "2022-03-29T11:39:24Z" + } + } + }, + "identity_profile_preview": { + "media": { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "type": "IMAGE", + "created": "2021-06-11T11:39:24Z", + "last_updated": "2021-06-11T11:39:24Z" + } + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/RTWIdentityProfileReport.json b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/RTWIdentityProfileReport.json new file mode 100644 index 0000000..c389acc --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/RTWIdentityProfileReport.json @@ -0,0 +1,118 @@ +{ + "identity_assertion": { + "current_name": { + "given_names": "JOHN JIM FRED", + "first_name": "JOHN", + "middle_name": "JIM FRED", + "family_name": "FOO", + "full_name": "JOHN JIM FRED FOO" + }, + "date_of_birth": "1979-01-01" + }, + "verification_report": { + "report_id": "61b99534-116d-4a25-9750-2d708c0fb168", + "timestamp": "2022-01-02T15:04:05Z", + "subject_id": "f0726cb6-97c1-4802-9feb-eb7c6cd07949", + "trust_framework": "UK_TFIDA", + "schemes_compliance": [ + { + "scheme": { + "type": "RTW" + }, + "requirements_met": true + } + ], + "assurance_process": { + "level_of_assurance": "MEDIUM", + "policy": "GPG45", + "procedure": "M1C", + "assurance": [ + { + "type": "EVIDENCE_STRENGTH", + "classification": "4", + "evidence_links": [ + "41960172-ca91-487c-8bb3-2c547f80fe54" + ] + }, + { + "type": "EVIDENCE_VALIDITY", + "classification": "3", + "evidence_links": [ + "41960172-ca91-487c-8bb3-2c547f80fe54" + ] + }, + { + "type": "VERIFICATION", + "classification": "3", + "evidence_links": [ + "41960172-ca91-487c-8bb3-2c547f80fe54", + "fb0880ca-5dd5-4776-badb-17549123c50b" + ] + } + ] + }, + "evidence": { + "documents": [ + { + "evidence_id": "41960172-ca91-487c-8bb3-2c547f80fe54", + "timestamp": "2022-01-02T15:04:05Z", + "document_fields": { + "full_name": "JOHN JIM FRED FOO", + "date_of_birth": "1979-01-01", + "nationality": "GBR", + "given_names": "JOHN JIM FRED", + "family_name": "FOO", + "place_of_birth": "SAMPLETOWN", + "gender": "MALE", + "document_type": "PASSPORT", + "issuing_country": "GBR", + "document_number": "123456789", + "expiration_date": "2030-01-01", + "date_of_issue": "2020-01-01", + "issuing_authority": "HMPO", + "mrz": { + "type": 2, + "line1": "P" +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/resource-container-static.json b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/resource-container-static.json new file mode 100644 index 0000000..7021a74 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/resource-container-static.json @@ -0,0 +1,52 @@ +{ + "liveness_capture": [ + { + "id": "1bf432f3-63fd-4fa7-b243-84adc305dd5f", + "tasks": [], + "source": { + "type": "END_USER" + }, + "image": { + "media": { + "id": "b73c5b58-cd54-4784-8a58-af936f31994f", + "type": "IMAGE", + "created": "2023-01-04T13:42:34Z", + "last_updated": "2023-01-04T13:42:34Z" + } + }, + "liveness_type": "STATIC" + }, + { + "id": "1fb5120f-7e50-4c48-8753-8311f4a96ed4", + "tasks": [], + "source": { + "type": "END_USER" + }, + "image": { + "media": { + "id": "72bfb8eb-2333-4c24-8e39-cd67c408e68c", + "type": "IMAGE", + "created": "2023-01-04T13:42:25Z", + "last_updated": "2023-01-04T13:42:25Z" + } + }, + "liveness_type": "STATIC" + }, + { + "id": "ea28dd83-c273-4d59-a91f-fd2d0df06640", + "tasks": [], + "source": { + "type": "END_USER" + }, + "image": { + "media": { + "id": "e95bc897-a8cf-4c21-ba87-4415571d1a55", + "type": "IMAGE", + "created": "2023-01-04T13:42:15Z", + "last_updated": "2023-01-04T13:42:15Z" + } + }, + "liveness_type": "STATIC" + } +] +} \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/resource-container.json b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/resource-container.json new file mode 100644 index 0000000..9b473a4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/resource-container.json @@ -0,0 +1,30 @@ +{ + "liveness_capture": [{ + "id": "a831bc40-e3c2-11ea-87d0-0242ac130003", + "liveness_type": "ZOOM", + "facemap": { + "media": { + "id": "abd059e2-e3c2-11ea-87d0-0242ac130003", + "type": "BINARY", + "created": "2020-08-21T16:25:00Z", + "last_updated": "2020-08-21T16:25:00Z" + } + }, + "frames": [{ + "media": { + "id": "b1177368-e3c2-11ea-87d0-0242ac130003", + "type": "IMAGE", + "created": "2020-08-21T16:25:00Z", + "last_updated": "2020-08-21T16:25:00Z" + } + }, { + "media": null + } + ] + }, { + "id": "d2677368-e3c2-11ea-87d0-0242ac139182", + "liveness_type": "OTHER_LIVENESS_TYPE" + } + ] +} + diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_driving_license.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_driving_license.txt new file mode 100644 index 0000000..dfe875f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_driving_license.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_passport.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_passport.txt new file mode 100644 index 0000000..237f052 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_passport.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_unknown.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_unknown.txt new file mode 100644 index 0000000..d06eb77 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_unknown.txt @@ -0,0 +1,39 @@ +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 +WWoW7TZ8Y5CjoA \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_yoti_admin.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_yoti_admin.txt new file mode 100644 index 0000000..6333815 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_anchor_yoti_admin.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_date_of_birth.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_date_of_birth.txt new file mode 100644 index 0000000..7cf4b6c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_date_of_birth.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_multivalue.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_multivalue.txt new file mode 100644 index 0000000..96c7af3 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_multivalue.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_third_party.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_third_party.txt new file mode 100644 index 0000000..4582559 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_attribute_third_party.txt @@ -0,0 +1 @@ +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 diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_extra_data.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_extra_data.txt new file mode 100644 index 0000000..25d50e1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_extra_data.txt @@ -0,0 +1 @@ +CmMIBhJfChFzb21lSXNzdWFuY2VUb2tlbhJKChgyMDE5LTEwLTE1VDIyOjA0OjA1LjEyM1oSEwoRY29tLnRoaXJkcGFydHkuaWQSGQoXY29tLnRoaXJkcGFydHkub3RoZXJfaWQ= \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_third_party_issuance_details.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_third_party_issuance_details.txt new file mode 100644 index 0000000..b8fd354 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/test_third_party_issuance_details.txt @@ -0,0 +1 @@ +ChFzb21lSXNzdWFuY2VUb2tlbhIvChgyMDE5LTEwLTE1VDIyOjA0OjA1LjEyM1oSEwoRY29tLnRoaXJkcGFydHkuaWQ= \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/watchlist_advanced_ca_profile_custom.json b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/watchlist_advanced_ca_profile_custom.json new file mode 100644 index 0000000..968e4cc --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/watchlist_advanced_ca_profile_custom.json @@ -0,0 +1,257 @@ +{ + "client_session_token_ttl": 155057, + "session_id": "a3819be3-df1f-4d8c-9161-abfe1b19d9e8", + "state": "COMPLETED", + "resources": { + "id_documents": [ + { + "id": "d99241db-243f-472d-84a2-d956b87db5f8", + "tasks": [ + { + "type": "ID_DOCUMENT_TEXT_DATA_EXTRACTION", + "id": "e1995b9a-9b6f-43b9-a179-32bbe2d25586", + "state": "DONE", + "created": "2022-01-14T14:54:14Z", + "last_updated": "2022-01-14T14:59:13Z", + "generated_checks": [ + { + "id": "851cec96-7459-49d3-b9b2-bcbc4c759987", + "type": "ID_DOCUMENT_TEXT_DATA_CHECK" + } + ], + "generated_media": [ + { + "id": "a8c9e973-4e58-43d8-952d-17f36f12bce2", + "type": "JSON" + } + ] + } + ], + "source": { + "type": "END_USER" + }, + "document_type": "DRIVING_LICENCE", + "issuing_country": "GBR", + "pages": [ + { + "capture_method": "UPLOAD", + "media": { + "id": "8c65cb1c-92cb-43df-bac1-65433ac3a1c1", + "type": "IMAGE", + "created": "2022-01-14T14:54:30Z", + "last_updated": "2022-01-14T14:54:30Z" + }, + "frames": [{}, {}] + }, + { + "capture_method": "UPLOAD", + "media": { + "id": "a7c8331c-6e29-4720-8818-27c02e6252b3", + "type": "IMAGE", + "created": "2022-01-14T14:54:31Z", + "last_updated": "2022-01-14T14:54:31Z" + }, + "frames": [{}, {}] + } + ], + "document_fields": { + "media": { + "id": "b5cae0f3-ae43-41d3-b8a7-1b0d363938ef", + "type": "JSON", + "created": "2022-01-14T14:59:13Z", + "last_updated": "2022-01-14T14:59:13Z" + } + }, + "document_id_photo": { + "media": { + "id": "ee3f9895-0552-4e16-ad9a-914e2f676c10", + "type": "IMAGE", + "created": "2022-01-14T14:54:34Z", + "last_updated": "2022-01-14T14:54:34Z" + } + } + } + ], + "supplementary_documents": [], + "liveness_capture": [], + "face_capture": [] + }, + "checks": [ + { + "type": "ID_DOCUMENT_AUTHENTICITY", + "id": "f11efbfb-712c-4f9d-8328-2add190f32e3", + "state": "DONE", + "resources_used": ["d99241db-243f-472d-84a2-d956b87db5f8"], + "generated_media": [], + "report": { + "recommendation": { + "value": "APPROVE" + }, + "breakdown": [ + { + "sub_check": "doc_number_validation", + "result": "PASS", + "details": [] + }, + { + "sub_check": "document_in_date", + "result": "PASS", + "details": [] + }, + { + "sub_check": "fraud_list_check", + "result": "PASS", + "details": [] + } + ] + }, + "created": "2022-01-14T14:54:36Z", + "last_updated": "2022-01-14T14:59:14Z" + }, + { + "type": "ID_DOCUMENT_TEXT_DATA_CHECK", + "id": "851cec96-7459-49d3-b9b2-bcbc4c759987", + "state": "DONE", + "resources_used": ["d99241db-243f-472d-84a2-d956b87db5f8"], + "generated_media": [ + { + "id": "b5cae0f3-ae43-41d3-b8a7-1b0d363938ef", + "type": "JSON" + } + ], + "report": { + "recommendation": { + "value": "APPROVE" + }, + "breakdown": [ + { + "sub_check": "text_data_readable", + "result": "PASS", + "details": [] + } + ] + }, + "created": "2022-01-14T14:54:36Z", + "last_updated": "2022-01-14T14:59:13Z" + }, + { + "type": "WATCHLIST_ADVANCED_CA", + "id": "06e661c5-0e24-44ed-8f6c-8b99807efc12", + "state": "DONE", + "resources_used": ["d99241db-243f-472d-84a2-d956b87db5f8"], + "generated_media": [ + { + "id": "7c405b5e-348d-4a2c-87ff-787d4fb139c0", + "type": "JSON" + } + ], + "report": { + "recommendation": { + "value": "CONSIDER", + "reason": "POTENTIAL_MATCH" + }, + "breakdown": [ + { + "sub_check": "adverse_media", + "result": "FAIL", + "details": [ + { + "name": "number_of_hits", + "value": "251" + }, + { + "name": "closest_match", + "value": "name_exact,year_of_birth" + } + ] + }, + { + "sub_check": "custom_search", + "result": "FAIL", + "details": [] + }, + { + "sub_check": "fitness_probity", + "result": "FAIL", + "details": [ + { + "name": "number_of_hits", + "value": "3" + }, + { + "name": "closest_match", + "value": "name_exact" + } + ] + }, + { + "sub_check": "pep", + "result": "FAIL", + "details": [ + { + "name": "number_of_hits", + "value": "13" + }, + { + "name": "closest_match", + "value": "name_exact,year_of_birth" + } + ] + }, + { + "sub_check": "warning", + "result": "FAIL", + "details": [ + { + "name": "number_of_hits", + "value": "9" + }, + { + "name": "closest_match", + "value": "name_exact,year_of_birth" + } + ] + } + ], + "watchlist_summary": { + "total_hits": 100, + "search_config": { + "type": "WITH_CUSTOM_ACCOUNT", + "remove_deceased": true, + "share_url": true, + "matching_strategy": { + "type": "FUZZY", + "fuzziness": 0.6 + }, + "sources": { + "type": "PROFILE", + "search_profile": "b41d82de-9a1d-4494-97a6-8b1b9895a908" + }, + "api_key": "gQ2vf0STnF5nGy9SSdyuGJuYMFfNASmV", + "client_ref": "111111", + "monitoring": true + }, + "raw_results": { + "media": { + "id": "0ebadb40-670a-4dd8-b0cc-5079d5d74c1c", + "type": "JSON", + "created": "2022-01-14T14:59:14Z", + "last_updated": "2022-01-14T14:59:14Z" + } + }, + "associated_country_codes": ["GBR"] + } + }, + "created": "2022-01-14T14:54:36Z", + "last_updated": "2022-01-14T14:59:14Z", + "generated_profile": { + "media": { + "id": "7c405b5e-348d-4a2c-87ff-787d4fb139c0", + "type": "JSON", + "created": "2022-01-14T14:59:14Z", + "last_updated": "2022-01-14T14:59:14Z" + } + } + } + ] +} \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/watchlist_screening.json b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/watchlist_screening.json new file mode 100644 index 0000000..3aa3aec --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/fixtures/watchlist_screening.json @@ -0,0 +1,135 @@ +{ + "client_session_token_ttl": 8785, + "session_id": "d7445779-5e70-4a27-8fec-dd7835106e88", + "state": "COMPLETED", + "resources": { + "id_documents": [ + { + "id": "20deead7-4cb6-4921-a56c-ea10fa0a5595", + "tasks": [ + { + "type": "ID_DOCUMENT_TEXT_DATA_EXTRACTION", + "id": "738e0007-68b8-466a-bd1d-30448b157cd8", + "state": "DONE", + "created": "2021-07-20T15:14:42Z", + "last_updated": "2021-07-20T15:15:53Z", + "generated_checks": [], + "generated_media": [ + { + "id": "cfd85b6d-e78f-4c14-994c-78ad0671312d", + "type": "JSON" + } + ] + } + ], + "source": { + "type": "END_USER" + }, + "document_type": "PASSPORT", + "issuing_country": "GBR", + "pages": [ + { + "capture_method": "UPLOAD", + "media": { + "id": "6b58c356-1e68-4ee2-af54-3264406de92a", + "type": "IMAGE", + "created": "2021-07-20T15:15:52Z", + "last_updated": "2021-07-20T15:15:52Z" + }, + "frames": [{}, {}, {}] + } + ], + "document_fields": { + "media": { + "id": "cfd85b6d-e78f-4c14-994c-78ad0671312d", + "type": "JSON", + "created": "2021-07-20T15:15:53Z", + "last_updated": "2021-07-20T15:15:53Z" + } + }, + "document_id_photo": { + "media": { + "id": "d8f8051e-c5d7-4334-b5af-6b9a07fdb38c", + "type": "IMAGE", + "created": "2021-07-20T15:15:53Z", + "last_updated": "2021-07-20T15:15:53Z" + } + } + } + ], + "supplementary_documents": [], + "liveness_capture": [], + "face_capture": [] + }, + "checks": [ + { + "type": "WATCHLIST_SCREENING", + "id": "8f4a89a1-4614-47ab-9506-2c82352b66d2", + "state": "DONE", + "resources_used": ["20deead7-4cb6-4921-a56c-ea10fa0a5595"], + "generated_media": [ + { + "id": "2f5bbedd-cc1b-4d9f-a424-90bb5ee0fc99", + "type": "JSON" + } + ], + "report": { + "recommendation": { + "value": "APPROVE" + }, + "breakdown": [ + { + "sub_check": "adverse_media", + "result": "PASS", + "details": [] + }, + { + "sub_check": "fitness_probity", + "result": "PASS", + "details": [] + }, + { + "sub_check": "pep", + "result": "PASS", + "details": [] + }, + { + "sub_check": "sanction", + "result": "PASS", + "details": [] + }, + { + "sub_check": "warning", + "result": "PASS", + "details": [] + } + ], + "watchlist_summary": { + "total_hits": 0, + "search_config": { + "categories": ["ADVERSE-MEDIA", "SANCTIONS"] + }, + "raw_results": { + "media": { + "id": "b0fb459c-f573-4205-9847-5ab72301325c", + "type": "JSON", + "created": "2021-07-20T15:15:55Z", + "last_updated": "2021-07-20T15:15:55Z" + } + }, + "associated_country_codes": ["GBR"] + } + }, + "created": "2021-07-20T15:15:54Z", + "last_updated": "2021-07-20T15:15:55Z", + "generated_profile": { + "media": { + "id": "2f5bbedd-cc1b-4d9f-a424-90bb5ee0fc99", + "type": "JSON", + "created": "2021-07-20T15:15:55Z", + "last_updated": "2021-07-20T15:15:55Z" + } + } + } + ] +} \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/key.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/key.go new file mode 100644 index 0000000..54e3f49 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/key.go @@ -0,0 +1,23 @@ +package test + +import ( + "crypto/rsa" + "os" + + "github.com/getyoti/yoti-go-sdk/v3/cryptoutil" +) + +// GetValidKey returns a parsed RSA Private Key from a test key +func GetValidKey(filepath string) (key *rsa.PrivateKey) { + keyBytes, err := os.ReadFile(filepath) + if err != nil { + panic("Error reading the test key: " + err.Error()) + } + + key, err = cryptoutil.ParseRSAKey(keyBytes) + if err != nil { + panic("Error parsing the test key: " + err.Error()) + } + + return key +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/test-key-invalid-format.pem b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/test-key-invalid-format.pem new file mode 100644 index 0000000..dad3162 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/test-key-invalid-format.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAu7VR2P4kfOBMbsfFeaoTH7QNHVfS/VoallsiHLR9r2u52EfA +cnGELiCO8Z4I4OjMMg9yrP2Wcpyq4+pUdFW7GG2NkkPaTQpAlQ5Hm6xxgTGKahju +0OOGdLbWwol0S+QLFcnadj7/0pCSKe/v1XGR/iGZgyORHDzRMQHbLlBtMkC+wOIi +CUgnWkWhi0AJNoaEQoSvGdVAKjCOAimSbID9vGmnuK7FXCauMFRjbBh/Cbeyz4xl +w+BSvQmAGWSxpLyRinWXekNFtrm3YURwxKl4lpkEH6QQMgrImxMg+NURxNAsZob5 +QumxFopXO7ib0gNes47Ct5KyRPeF8CF+VLl6k9KWvQ3v7SYoypTXXwYfTbpqPhBr +mbPdqpIZ2oUKZJPRek68aU17YalAZ0jPNA30+UD2oKynYU2mif9UnrDxUnTnBnQX +F9tAsDWo/pOXwjKOYTKsxyBhbgh8rrgPFAqz00+qbk21gaiP4tjNMByBBMHzXUOg +GqlMjNNQhejTjL6rlXbJgmQDXPG4Xi+Q/+sUkrLNOTKY3FdnTw5PFUw9sRbP6+D6 +jS799P/OKay1DxuOPLw6r5QnfR2+pk9mNmgjVcBwwqt7gaUEjvDvj60ZppLZqQ8X +7Bv3zQetQHBtmhXyiuIH/UWXuj/VKLjnbaJtzZYTp8W4X8OT6vEwhLS9AncCAwEA +AQKCAgAIGBeBbeQQ5nMlS8P+LRFKCq+OFl1ow1vmI+PirP3GdLS82Ms5pB95Bbpk +PNZRLHixp+zf/MdiBdNwpIgjxBafRQoXxolBTTHfu4/m7JawZXx8errBky4XFlNI +bDjxlNHNjLi45JqPb+B9onULFSygcr514zC8sPqsTFIxOxKaWiRfmOCy2cOoptwC +by52hXJqk+IhEQsFRra47SX9O8q1NzEeS5sDED?uoZTv8lZ4Cs3RGVLCEYg/0osN +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 +-----END RSA PRIVATE KEY----- diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/test-key.pem b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/test-key.pem new file mode 100644 index 0000000..98c7641 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/test/test-key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAu7VR2P4kfOBMbsfFeaoTH7QNHVfS/VoallsiHLR9r2u52EfA +cnGELiCO8Z4I4OjMMg9yrP2Wcpyq4+pUdFW7GG2NkkPaTQpAlQ5Hm6xxgTGKahju +0OOGdLbWwol0S+QLFcnadj7/0pCSKe/v1XGR/iGZgyORHDzRMQHbLlBtMkC+wOIi +CUgnWkWhi0AJNoaEQoSvGdVAKjCOAimSbID9vGmnuK7FXCauMFRjbBh/Cbeyz4xl +w+BSvQmAGWSxpLyRinWXekNFtrm3YURwxKl4lpkEH6QQMgrImxMg+NURxNAsZob5 +QumxFopXO7ib0gNes47Ct5KyRPeF8CF+VLl6k9KWvQ3v7SYoypTXXwYfTbpqPhBr +mbPdqpIZ2oUKZJPRek68aU17YalAZ0jPNA30+UD2oKynYU2mif9UnrDxUnTnBnQX +F9tAsDWo/pOXwjKOYTKsxyBhbgh8rrgPFAqz00+qbk21gaiP4tjNMByBBMHzXUOg +GqlMjNNQhejTjL6rlXbJgmQDXPG4Xi+Q/+sUkrLNOTKY3FdnTw5PFUw9sRbP6+D6 +jS799P/OKay1DxuOPLw6r5QnfR2+pk9mNmgjVcBwwqt7gaUEjvDvj60ZppLZqQ8X +7Bv3zQetQHBtmhXyiuIH/UWXuj/VKLjnbaJtzZYTp8W4X8OT6vEwhLS9AncCAwEA +AQKCAgAIGBeBbeQQ5nMlS8P+LRFKCq+OFl1ow1vmI+PirP3GdLS82Ms5pB95Bbpk +PNZRLHixp+zf/MdiBdNwpIgjxBafRQoXxolBTTHfu4/m7JawZXx8errBky4XFlNI +bDjxlNHNjLi45JqPb+B9onULFSygcr514zC8sPqsTFIxOxKaWiRfmOCy2cOoptwC +by52hXJqk+IhEQsFRra47SX9O8q1NzEeS5sDED/uoZTv8lZ4Cs3RGVLCEYg/0osN +jUQDwIXeHJf9k60L5hI8RYE/WbdzdwGwg5iXL9ParAZ99GIhxIBFo4hYFE+okyqT +zrAZbD/HKl7HH7JEOxAxfKA/8weQCVlsyAyMXJE8RD7IXgId0AcH2SNj8C2NkaJS +aYAkcmN0qvm60OnOCjKULToF/AK0hymF8LjftFsMQ+RaAQJ1bJpKZ+tr58hRakUX +FtUx+DquC137GSQuBRHf63J5DrOgosltCL0aaAqTYp/rtN79ktfIY3k/13gYC3jm +jqzAPR7p/lMVJri0x1rlcN1d3mpHV9bQqSvRpzvCcxym8yv9I7njtlpULi8lu/jd +Vw1eb/J9mmicNHE/mbF4afUjrGCudQ6Fu/opLYvHrM+nBcuTd6EUMtIxvs74XPeB +JC5R36q8x/EFp983RMDjN2Uv4P05SFxG/CG849QVDvRrp29KkQKCAQEA4LOIrYLY +kw72PAccYLzXpV6UwuKdpPbwXVG+sj60YZ4WzhRVHF2Xjzc3nKkGID4DS7vWvl7o +QeTwHddyxIzdcE0JzBUc7vUq3hGGGPb+arbPJeayrW04GHfJpDYAlfEv2ear/yis +HJ5SCCTDSVeV9fjRg3VqutKJU+/RtlMHQet6dPqjq4DfQF8nIDfK3uaQR2llXEwa +scEAxL2igJNgk0omvq+F76wIy7kHOVuKwYvE3E4ig8cxYRsHdbbIxW9JHnzoX6j2 +n2VjZO2ciBPWLDuBdWRdjKjfAzpR8eWo0FqElt0nUqjpI65ZuBUBvdnMTQtLPvsf +GTV40I5lj+flRQKCAQEA1dqtcvd18hKwKLUHVIluGPR7c0nWmBjBKhotO3bnOaNC +TvqIREO/9KhnrE/ry/QmKZWjf9+3E9dJLEIeNkUTFmHpnScjCOZ/fcYXMfN9LLKW +CA+YPh3GlUKV9y/QIkODiSUQ6/qFud+ACcp0uY2VCi4RtMkYleNR/E1/JsnQgVtF +eI+v/tGHShu5hwgn13HKbQGV4GbzvLZHJII5YQyqCjkvGWlq2NYBqW34+BYqjQRS +G9+hzcDbr39gNzZBeQA/kQO5dVIqqdxL7HQa3zdXcrT/keATFsMjSdnUQJ441kwS +Xu7nQsCDkeas6q6dVm/tNmlZaMerDe1P+QDSKF7OiwKCAQEApvagc5VLShKPAtGh +03venOFnllv/GYnn1t+b3CRdsj9e4KgZCee9a0xzRTQO+jw6BLdBfNlWqUfs56+k +dsnY7M5BnmR9yE1iGfpZcwlsyGyoBZijYdxLF1tC+IKr8r5xeO8/FGzrXqSBfc2b +Uk8Dfe7x90VzFfjE1BrZ8ClHtkK8DloC7bfnq5RIpVbvpqsZwAZfq7JdD4HDCW2D +ZxibZTZvDbesxQdGzeHhrUwJEYHCuJRSbyq+1VHZPC2ih5oGceIMZLBO+OfEcEVi +z3Y16U4aBtmZ7Z+5flOCekTVKGRqKxOPWYtrGPk/b1okniZM+V6P/e9pDzk9WXLF +oqWEJQKCAQEAjX6suJ6m+U4IJEby3Ko5oGVSsQsv416toA/F0cxwXSB6JQt60cAJ +5/Ts84PFviKChY0uqtL4rTYKgjAVEU9Ou8Z47bQRaDgqLqu8eR5juglHX3oB/0dw +Nx3hX7XQ/nqxMzLFKX2OsVcBvnioFoVpEV099eIAVFwdyNP1x1JMlOow4v4fMnis +DQqfDIsG4XO2vb0Iz3sO1dO86pkHIgFhGHaRhTzMpz+hxdqvmmYALWGoeizTP/HU +6R9cJ+vMEiVp6acPNGLzO4Q47/A6P2q8f3bmijw6JRtj498uorqNXKzkks97UB1U +cFqyGm0CSUixKQk3US6bLRHRki1K388q1QKCAQAvgn2I9hPshEATeSlxlgCfwZjo +EocJ0tiCglyWv+X2k5xv/7p5/5gF1FD2HnDRLAtvfKPj2E9zLdE/Qr1BVUQjzdun +Vm34+MQU855HbXpxznlgaymEyb0EYvkXa6BTO7XHkNrIVqwGJjqOV14+63SYku+r +PHvR9VNTjZcru/JqOMscbFAHhyMhLANtjQh6WYZ//ESVilqUfuxh9nZzy5XzXf6B +GuxYE5vRyqXYuHe3MNpZOKqdAAiiD4+qW/45pyDV6ZxsS06pzCS9cMI9N7QxnbFB +p1ZtrW/+lEq1O5/iWZDisbhTJh+QWp7NK4GdLB5BMSXFsqQx4SI7zPVki64t +-----END RSA PRIVATE KEY----- diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/util/conversion.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/util/conversion.go new file mode 100644 index 0000000..4b14616 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/util/conversion.go @@ -0,0 +1,16 @@ +package util + +import ( + "encoding/base64" +) + +// Base64ToBytes converts a base64 string to bytes +func Base64ToBytes(base64Str string) ([]byte, error) { + return base64.StdEncoding.DecodeString(base64Str) +} + +// UrlSafeBase64ToBytes UrlSafe Base64 uses '-' and '_', instead of '+' and '/' respectively, so it can be passed +// as a url parameter without extra encoding. +func UrlSafeBase64ToBytes(urlSafeBase64 string) ([]byte, error) { + return base64.URLEncoding.DecodeString(urlSafeBase64) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/activitydetails.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/activitydetails.go new file mode 100644 index 0000000..1208166 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/activitydetails.go @@ -0,0 +1,14 @@ +package yotierror + +import "errors" + +var ( + // InvalidTokenError means that the token used to call GetActivityDetails is invalid. Make sure you are retrieving this token correctly. + InvalidTokenError = errors.New("invalid token") + + // TokenDecryptError means that the token could not be decrypted. Ensure you are using the correct .pem file. + TokenDecryptError = errors.New("unable to decrypt token") + + // SharingFailureError means that the share between a user and an application was not successful. + SharingFailureError = DetailedSharingFailureError{} +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/multi_error.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/multi_error.go new file mode 100644 index 0000000..fe8d7ea --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/multi_error.go @@ -0,0 +1,21 @@ +package yotierror + +import "fmt" + +// MultiError wraps one or more errors into a single error +type MultiError struct { + This error + Next error +} + +func (e MultiError) Error() string { + if e.Next != nil { + return fmt.Sprintf("%s, %s", e.This.Error(), e.Next.Error()) + } + return e.This.Error() +} + +// Unwrap the next error +func (e MultiError) Unwrap() error { + return e.Next +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/multi_error_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/multi_error_test.go new file mode 100644 index 0000000..fa1923f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/multi_error_test.go @@ -0,0 +1,17 @@ +package yotierror + +import ( + "errors" + "testing" + + "gotest.tools/v3/assert" +) + +func TestMultiError(t *testing.T) { + err := errors.New("inner err") + err = MultiError{This: errors.New("outer err"), Next: err} + result := err.(MultiError) + + assert.Equal(t, result.Error(), "outer err, inner err") + assert.Error(t, result.Unwrap(), "inner err") +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/response.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/response.go new file mode 100644 index 0000000..75c689d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/response.go @@ -0,0 +1,137 @@ +package yotierror + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +var ( + defaultUnknownErrorMessageConst = "unknown HTTP error" + + // DefaultHTTPErrorMessages maps HTTP error status codes to format strings + // to create useful error messages. -1 is used to specify a default message + // that can be used if an error code is not explicitly defined + DefaultHTTPErrorMessages = map[int]string{ + -1: defaultUnknownErrorMessageConst, + } +) + +// DataObject maps from JSON error responses +type DataObject struct { + Code string `json:"code"` + Message string `json:"message"` + Errors []ItemDataObject `json:"errors,omitempty"` +} + +// ItemDataObject maps from JSON error items +type ItemDataObject struct { + Message string `json:"message"` + Property string `json:"property"` +} + +// Error indicates errors related to the Yoti API. +type Error struct { + message string + Err error + Response *http.Response +} + +// NewResponseError creates a new Error +func NewResponseError(response *http.Response, httpErrorMessages ...map[int]string) *Error { + return &Error{ + message: formatResponseMessage(response, httpErrorMessages...), + Response: response, + } +} + +// Error return the error message +func (e Error) Error() string { + return e.message +} + +// Temporary indicates this error is a temporary error +func (e Error) Temporary() bool { + return e.Response != nil && e.Response.StatusCode >= 500 +} + +func formatResponseMessage(response *http.Response, httpErrorMessages ...map[int]string) string { + defaultMessage := handleHTTPError(response, httpErrorMessages...) + + if response == nil || response.Body == nil { + return defaultMessage + } + + body, _ := io.ReadAll(response.Body) + response.Body = io.NopCloser(bytes.NewBuffer(body)) + + var errorDO DataObject + jsonError := json.Unmarshal(body, &errorDO) + + if jsonError != nil || errorDO.Code == "" || errorDO.Message == "" { + return defaultMessage + } + + formattedCodeMessage := fmt.Sprintf("%d: %s - %s", response.StatusCode, errorDO.Code, errorDO.Message) + + var formattedItems []string + for _, item := range errorDO.Errors { + if item.Message != "" && item.Property != "" { + formattedItems = append( + formattedItems, + fmt.Sprintf("%s: `%s`", item.Property, item.Message), + ) + } + } + + if len(formattedItems) > 0 { + return fmt.Sprintf("%s: %s", formattedCodeMessage, strings.Join(formattedItems, ", ")) + } + + return formattedCodeMessage +} + +func formatHTTPError(message string, statusCode int, body []byte) string { + if len(body) == 0 { + return fmt.Sprintf("%d: %s", statusCode, message) + } + return fmt.Sprintf("%d: %s - %s", statusCode, message, body) +} + +func handleHTTPError(response *http.Response, errorMessages ...map[int]string) string { + var body []byte + if response.Body != nil { + body, _ = io.ReadAll(response.Body) + response.Body = io.NopCloser(bytes.NewBuffer(body)) + } else { + body = make([]byte, 0) + } + for _, handler := range errorMessages { + for code, message := range handler { + if code == response.StatusCode { + return formatHTTPError( + message, + response.StatusCode, + body, + ) + } + + } + if defaultMessage, ok := handler[-1]; ok { + return formatHTTPError( + defaultMessage, + response.StatusCode, + body, + ) + } + + } + return formatHTTPError( + defaultUnknownErrorMessageConst, + response.StatusCode, + body, + ) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/response_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/response_test.go new file mode 100644 index 0000000..f94810e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/response_test.go @@ -0,0 +1,151 @@ +package yotierror + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestError_ShouldReturnFormattedError(t *testing.T) { + jsonBytes, err := json.Marshal(DataObject{ + Code: "SOME_CODE", + Message: "some message", + Errors: []ItemDataObject{ + { + Message: "some property message", + Property: "some.property", + }, + }, + }) + assert.NilError(t, err) + + err = NewResponseError( + &http.Response{ + StatusCode: 401, + Body: io.NopCloser(bytes.NewReader(jsonBytes)), + }, + ) + + assert.ErrorContains(t, err, "SOME_CODE - some message: some.property: `some property message`") +} + +func TestError_ShouldReturnFormattedErrorCodeAndMessageOnly(t *testing.T) { + jsonBytes, err := json.Marshal(DataObject{ + Code: "SOME_CODE", + Message: "some message", + }) + assert.NilError(t, err) + + err = NewResponseError( + &http.Response{ + StatusCode: 400, + Body: io.NopCloser(bytes.NewReader(jsonBytes)), + }, + ) + + assert.ErrorContains(t, err, "400: SOME_CODE - some message") +} + +func TestError_ShouldReturnFormattedError_ReturnWrappedErrorByDefault(t *testing.T) { + err := NewResponseError( + &http.Response{ + StatusCode: 401, + }, + ) + + assert.ErrorContains(t, err, "401: unknown HTTP error") +} + +func TestError_ShouldReturnFormattedError_ReturnWrappedErrorWhenInvalidJSON(t *testing.T) { + response := &http.Response{ + StatusCode: 400, + Body: io.NopCloser(strings.NewReader("some invalid JSON")), + } + err := NewResponseError( + response, + ) + + assert.ErrorContains(t, err, "400: unknown HTTP error - some invalid JSON") + + errorResponse := err.Response + assert.Equal(t, response, errorResponse) + + body, readErr := io.ReadAll(errorResponse.Body) + assert.NilError(t, readErr) + + assert.Equal(t, string(body), "some invalid JSON") +} + +func TestError_ShouldReturnFormattedError_IgnoreUnknownErrorItems(t *testing.T) { + jsonString := "{\"message\": \"some message\", \"code\": \"SOME_CODE\", \"error\": [{\"some_key\": \"some value\"}]}" + response := &http.Response{ + StatusCode: 400, + Body: io.NopCloser(strings.NewReader(jsonString)), + } + err := NewResponseError( + response, + ) + + assert.ErrorContains(t, err, "400: SOME_CODE - some message") + + errorResponse := err.Response + assert.Equal(t, response, errorResponse) + + body, readErr := io.ReadAll(errorResponse.Body) + assert.NilError(t, readErr) + + assert.Equal(t, string(body), jsonString) +} + +func TestError_ShouldReturnCustomErrorForCode(t *testing.T) { + response := &http.Response{ + StatusCode: 404, + Body: io.NopCloser(strings.NewReader("some body")), + } + err := NewResponseError( + response, + map[int]string{404: "some message"}, + ) + + assert.ErrorContains(t, err, "404: some message - some body") +} + +func TestError_ShouldReturnCustomDefaultError(t *testing.T) { + response := &http.Response{ + StatusCode: 500, + Body: io.NopCloser(strings.NewReader("some body")), + } + err := NewResponseError( + response, + map[int]string{-1: "some default message"}, + ) + + assert.ErrorContains(t, err, "500: some default message - some body") +} + +func TestError_ShouldReturnTemporaryForServerError(t *testing.T) { + response := &http.Response{ + StatusCode: 500, + } + err := NewResponseError( + response, + ) + + assert.Check(t, err.Temporary()) +} + +func TestError_ShouldNotReturnTemporaryForClientError(t *testing.T) { + response := &http.Response{ + StatusCode: 400, + } + err := NewResponseError( + response, + ) + + assert.Check(t, !err.Temporary()) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/sharing_failure_error.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/sharing_failure_error.go new file mode 100644 index 0000000..07dfd11 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotierror/sharing_failure_error.go @@ -0,0 +1,10 @@ +package yotierror + +type DetailedSharingFailureError struct { + Code *string + Description *string +} + +func (d DetailedSharingFailureError) Error() string { + return "sharing failure" +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/Attribute.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/Attribute.pb.go new file mode 100644 index 0000000..b53c647 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/Attribute.pb.go @@ -0,0 +1,670 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: Attribute.proto + +package yotiprotoattr + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Attribute struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + ContentType ContentType `protobuf:"varint,3,opt,name=content_type,json=contentType,proto3,enum=attrpubapi_v1.ContentType" json:"content_type,omitempty"` + Anchors []*Anchor `protobuf:"bytes,4,rep,name=anchors,proto3" json:"anchors,omitempty"` + UserMetadata []*UserMetadata `protobuf:"bytes,5,rep,name=user_metadata,json=userMetadata,proto3" json:"user_metadata,omitempty"` + Metadata *Metadata `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` + EphemeralId string `protobuf:"bytes,7,opt,name=ephemeral_id,json=ephemeralId,proto3" json:"ephemeral_id,omitempty"` +} + +func (x *Attribute) Reset() { + *x = Attribute{} + if protoimpl.UnsafeEnabled { + mi := &file_Attribute_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Attribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Attribute) ProtoMessage() {} + +func (x *Attribute) ProtoReflect() protoreflect.Message { + mi := &file_Attribute_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Attribute.ProtoReflect.Descriptor instead. +func (*Attribute) Descriptor() ([]byte, []int) { + return file_Attribute_proto_rawDescGZIP(), []int{0} +} + +func (x *Attribute) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Attribute) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *Attribute) GetContentType() ContentType { + if x != nil { + return x.ContentType + } + return ContentType_UNDEFINED +} + +func (x *Attribute) GetAnchors() []*Anchor { + if x != nil { + return x.Anchors + } + return nil +} + +func (x *Attribute) GetUserMetadata() []*UserMetadata { + if x != nil { + return x.UserMetadata + } + return nil +} + +func (x *Attribute) GetMetadata() *Metadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Attribute) GetEphemeralId() string { + if x != nil { + return x.EphemeralId + } + return "" +} + +type Metadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SupersededTimeStamp string `protobuf:"bytes,1,opt,name=superseded_time_stamp,json=supersededTimeStamp,proto3" json:"superseded_time_stamp,omitempty"` + Deletable bool `protobuf:"varint,2,opt,name=deletable,proto3" json:"deletable,omitempty"` + ReceiptId []byte `protobuf:"bytes,3,opt,name=receipt_id,json=receiptId,proto3" json:"receipt_id,omitempty"` + Revoked bool `protobuf:"varint,4,opt,name=revoked,proto3" json:"revoked,omitempty"` + Locked bool `protobuf:"varint,5,opt,name=locked,proto3" json:"locked,omitempty"` +} + +func (x *Metadata) Reset() { + *x = Metadata{} + if protoimpl.UnsafeEnabled { + mi := &file_Attribute_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Metadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Metadata) ProtoMessage() {} + +func (x *Metadata) ProtoReflect() protoreflect.Message { + mi := &file_Attribute_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Metadata.ProtoReflect.Descriptor instead. +func (*Metadata) Descriptor() ([]byte, []int) { + return file_Attribute_proto_rawDescGZIP(), []int{1} +} + +func (x *Metadata) GetSupersededTimeStamp() string { + if x != nil { + return x.SupersededTimeStamp + } + return "" +} + +func (x *Metadata) GetDeletable() bool { + if x != nil { + return x.Deletable + } + return false +} + +func (x *Metadata) GetReceiptId() []byte { + if x != nil { + return x.ReceiptId + } + return nil +} + +func (x *Metadata) GetRevoked() bool { + if x != nil { + return x.Revoked + } + return false +} + +func (x *Metadata) GetLocked() bool { + if x != nil { + return x.Locked + } + return false +} + +type Anchor struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ArtifactLink []byte `protobuf:"bytes,1,opt,name=artifact_link,json=artifactLink,proto3" json:"artifact_link,omitempty"` + OriginServerCerts [][]byte `protobuf:"bytes,2,rep,name=origin_server_certs,json=originServerCerts,proto3" json:"origin_server_certs,omitempty"` + ArtifactSignature []byte `protobuf:"bytes,3,opt,name=artifact_signature,json=artifactSignature,proto3" json:"artifact_signature,omitempty"` + SubType string `protobuf:"bytes,4,opt,name=sub_type,json=subType,proto3" json:"sub_type,omitempty"` + Signature []byte `protobuf:"bytes,5,opt,name=signature,proto3" json:"signature,omitempty"` + SignedTimeStamp []byte `protobuf:"bytes,6,opt,name=signed_time_stamp,json=signedTimeStamp,proto3" json:"signed_time_stamp,omitempty"` + AssociatedSource string `protobuf:"bytes,7,opt,name=associated_source,json=associatedSource,proto3" json:"associated_source,omitempty"` +} + +func (x *Anchor) Reset() { + *x = Anchor{} + if protoimpl.UnsafeEnabled { + mi := &file_Attribute_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Anchor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Anchor) ProtoMessage() {} + +func (x *Anchor) ProtoReflect() protoreflect.Message { + mi := &file_Attribute_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Anchor.ProtoReflect.Descriptor instead. +func (*Anchor) Descriptor() ([]byte, []int) { + return file_Attribute_proto_rawDescGZIP(), []int{2} +} + +func (x *Anchor) GetArtifactLink() []byte { + if x != nil { + return x.ArtifactLink + } + return nil +} + +func (x *Anchor) GetOriginServerCerts() [][]byte { + if x != nil { + return x.OriginServerCerts + } + return nil +} + +func (x *Anchor) GetArtifactSignature() []byte { + if x != nil { + return x.ArtifactSignature + } + return nil +} + +func (x *Anchor) GetSubType() string { + if x != nil { + return x.SubType + } + return "" +} + +func (x *Anchor) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +func (x *Anchor) GetSignedTimeStamp() []byte { + if x != nil { + return x.SignedTimeStamp + } + return nil +} + +func (x *Anchor) GetAssociatedSource() string { + if x != nil { + return x.AssociatedSource + } + return "" +} + +type UserMetadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *UserMetadata) Reset() { + *x = UserMetadata{} + if protoimpl.UnsafeEnabled { + mi := &file_Attribute_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserMetadata) ProtoMessage() {} + +func (x *UserMetadata) ProtoReflect() protoreflect.Message { + mi := &file_Attribute_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserMetadata.ProtoReflect.Descriptor instead. +func (*UserMetadata) Descriptor() ([]byte, []int) { + return file_Attribute_proto_rawDescGZIP(), []int{3} +} + +func (x *UserMetadata) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *UserMetadata) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type MultiValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Values []*MultiValue_Value `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` +} + +func (x *MultiValue) Reset() { + *x = MultiValue{} + if protoimpl.UnsafeEnabled { + mi := &file_Attribute_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MultiValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MultiValue) ProtoMessage() {} + +func (x *MultiValue) ProtoReflect() protoreflect.Message { + mi := &file_Attribute_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MultiValue.ProtoReflect.Descriptor instead. +func (*MultiValue) Descriptor() ([]byte, []int) { + return file_Attribute_proto_rawDescGZIP(), []int{4} +} + +func (x *MultiValue) GetValues() []*MultiValue_Value { + if x != nil { + return x.Values + } + return nil +} + +type MultiValue_Value struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ContentType ContentType `protobuf:"varint,1,opt,name=content_type,json=contentType,proto3,enum=attrpubapi_v1.ContentType" json:"content_type,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *MultiValue_Value) Reset() { + *x = MultiValue_Value{} + if protoimpl.UnsafeEnabled { + mi := &file_Attribute_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MultiValue_Value) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MultiValue_Value) ProtoMessage() {} + +func (x *MultiValue_Value) ProtoReflect() protoreflect.Message { + mi := &file_Attribute_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MultiValue_Value.ProtoReflect.Descriptor instead. +func (*MultiValue_Value) Descriptor() ([]byte, []int) { + return file_Attribute_proto_rawDescGZIP(), []int{4, 0} +} + +func (x *MultiValue_Value) GetContentType() ContentType { + if x != nil { + return x.ContentType + } + return ContentType_UNDEFINED +} + +func (x *MultiValue_Value) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_Attribute_proto protoreflect.FileDescriptor + +var file_Attribute_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, + 0x1a, 0x11, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0xbf, 0x02, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3d, 0x0a, 0x0c, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x1a, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, + 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x61, 0x6e, + 0x63, 0x68, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x74, + 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x41, 0x6e, 0x63, 0x68, + 0x6f, 0x72, 0x52, 0x07, 0x61, 0x6e, 0x63, 0x68, 0x6f, 0x72, 0x73, 0x12, 0x40, 0x0a, 0x0d, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x0c, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x33, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, + 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, + 0x72, 0x61, 0x6c, 0x49, 0x64, 0x22, 0xad, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x75, 0x70, 0x65, 0x72, 0x73, 0x65, 0x64, 0x65, 0x64, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x13, 0x73, 0x75, 0x70, 0x65, 0x72, 0x73, 0x65, 0x64, 0x65, 0x64, 0x54, 0x69, 0x6d, + 0x65, 0x53, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x61, + 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, + 0x74, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x12, 0x16, 0x0a, + 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, + 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x22, 0x9e, 0x02, 0x0a, 0x06, 0x41, 0x6e, 0x63, 0x68, 0x6f, 0x72, + 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x6c, 0x69, 0x6e, + 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x2e, 0x0a, 0x13, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0c, 0x52, 0x11, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x43, 0x65, 0x72, 0x74, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x11, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x2a, 0x0a, + 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x54, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x73, 0x73, + 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x36, 0x0a, 0x0c, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xa1, + 0x01, 0x0a, 0x0a, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x37, 0x0a, + 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x4d, 0x75, + 0x6c, 0x74, 0x69, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x5a, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, + 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x42, 0xe0, 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, 0x74, 0x69, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, 0x69, 0x2e, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x09, 0x41, 0x74, 0x74, + 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x2d, + 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x61, 0x74, 0x74, 0x72, 0xaa, 0x02, 0x1c, 0x59, 0x6f, 0x74, 0x69, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0xca, 0x02, 0x18, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, + 0x69, 0xe2, 0x02, 0x24, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1a, 0x59, 0x6f, 0x74, 0x69, 0x3a, + 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3a, 0x3a, 0x41, 0x74, 0x74, 0x72, 0x70, + 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_Attribute_proto_rawDescOnce sync.Once + file_Attribute_proto_rawDescData = file_Attribute_proto_rawDesc +) + +func file_Attribute_proto_rawDescGZIP() []byte { + file_Attribute_proto_rawDescOnce.Do(func() { + file_Attribute_proto_rawDescData = protoimpl.X.CompressGZIP(file_Attribute_proto_rawDescData) + }) + return file_Attribute_proto_rawDescData +} + +var file_Attribute_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_Attribute_proto_goTypes = []interface{}{ + (*Attribute)(nil), // 0: attrpubapi_v1.Attribute + (*Metadata)(nil), // 1: attrpubapi_v1.Metadata + (*Anchor)(nil), // 2: attrpubapi_v1.Anchor + (*UserMetadata)(nil), // 3: attrpubapi_v1.UserMetadata + (*MultiValue)(nil), // 4: attrpubapi_v1.MultiValue + (*MultiValue_Value)(nil), // 5: attrpubapi_v1.MultiValue.Value + (ContentType)(0), // 6: attrpubapi_v1.ContentType +} +var file_Attribute_proto_depIdxs = []int32{ + 6, // 0: attrpubapi_v1.Attribute.content_type:type_name -> attrpubapi_v1.ContentType + 2, // 1: attrpubapi_v1.Attribute.anchors:type_name -> attrpubapi_v1.Anchor + 3, // 2: attrpubapi_v1.Attribute.user_metadata:type_name -> attrpubapi_v1.UserMetadata + 1, // 3: attrpubapi_v1.Attribute.metadata:type_name -> attrpubapi_v1.Metadata + 5, // 4: attrpubapi_v1.MultiValue.values:type_name -> attrpubapi_v1.MultiValue.Value + 6, // 5: attrpubapi_v1.MultiValue.Value.content_type:type_name -> attrpubapi_v1.ContentType + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_Attribute_proto_init() } +func file_Attribute_proto_init() { + if File_Attribute_proto != nil { + return + } + file_ContentType_proto_init() + if !protoimpl.UnsafeEnabled { + file_Attribute_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Attribute); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_Attribute_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Metadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_Attribute_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Anchor); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_Attribute_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserMetadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_Attribute_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MultiValue); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_Attribute_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MultiValue_Value); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_Attribute_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_Attribute_proto_goTypes, + DependencyIndexes: file_Attribute_proto_depIdxs, + MessageInfos: file_Attribute_proto_msgTypes, + }.Build() + File_Attribute_proto = out.File + file_Attribute_proto_rawDesc = nil + file_Attribute_proto_goTypes = nil + file_Attribute_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/ContentType.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/ContentType.pb.go new file mode 100644 index 0000000..2fbf1e9 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/ContentType.pb.go @@ -0,0 +1,164 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: ContentType.proto + +package yotiprotoattr + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ContentType int32 + +const ( + ContentType_UNDEFINED ContentType = 0 + ContentType_STRING ContentType = 1 + ContentType_JPEG ContentType = 2 + ContentType_DATE ContentType = 3 + ContentType_PNG ContentType = 4 + ContentType_JSON ContentType = 5 + ContentType_MULTI_VALUE ContentType = 6 + ContentType_INT ContentType = 7 +) + +// Enum value maps for ContentType. +var ( + ContentType_name = map[int32]string{ + 0: "UNDEFINED", + 1: "STRING", + 2: "JPEG", + 3: "DATE", + 4: "PNG", + 5: "JSON", + 6: "MULTI_VALUE", + 7: "INT", + } + ContentType_value = map[string]int32{ + "UNDEFINED": 0, + "STRING": 1, + "JPEG": 2, + "DATE": 3, + "PNG": 4, + "JSON": 5, + "MULTI_VALUE": 6, + "INT": 7, + } +) + +func (x ContentType) Enum() *ContentType { + p := new(ContentType) + *p = x + return p +} + +func (x ContentType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ContentType) Descriptor() protoreflect.EnumDescriptor { + return file_ContentType_proto_enumTypes[0].Descriptor() +} + +func (ContentType) Type() protoreflect.EnumType { + return &file_ContentType_proto_enumTypes[0] +} + +func (x ContentType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ContentType.Descriptor instead. +func (ContentType) EnumDescriptor() ([]byte, []int) { + return file_ContentType_proto_rawDescGZIP(), []int{0} +} + +var File_ContentType_proto protoreflect.FileDescriptor + +var file_ContentType_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, + 0x76, 0x31, 0x2a, 0x69, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, + 0x4a, 0x50, 0x45, 0x47, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, + 0x12, 0x07, 0x0a, 0x03, 0x50, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x4a, 0x53, 0x4f, + 0x4e, 0x10, 0x05, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x55, 0x4c, 0x54, 0x49, 0x5f, 0x56, 0x41, 0x4c, + 0x55, 0x45, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x49, 0x4e, 0x54, 0x10, 0x07, 0x42, 0xe7, 0x01, + 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, 0x74, 0x69, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x10, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, 0x74, + 0x69, 0x2d, 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, 0x69, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x74, 0x74, 0x72, 0xaa, 0x02, 0x1c, 0x59, 0x6f, 0x74, 0x69, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0xca, 0x02, 0x18, 0x59, 0x6f, 0x74, 0x69, 0x5c, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, + 0x61, 0x70, 0x69, 0xe2, 0x02, 0x24, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, 0x47, + 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1a, 0x59, 0x6f, 0x74, + 0x69, 0x3a, 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3a, 0x3a, 0x41, 0x74, 0x74, + 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_ContentType_proto_rawDescOnce sync.Once + file_ContentType_proto_rawDescData = file_ContentType_proto_rawDesc +) + +func file_ContentType_proto_rawDescGZIP() []byte { + file_ContentType_proto_rawDescOnce.Do(func() { + file_ContentType_proto_rawDescData = protoimpl.X.CompressGZIP(file_ContentType_proto_rawDescData) + }) + return file_ContentType_proto_rawDescData +} + +var file_ContentType_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_ContentType_proto_goTypes = []interface{}{ + (ContentType)(0), // 0: attrpubapi_v1.ContentType +} +var file_ContentType_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_ContentType_proto_init() } +func file_ContentType_proto_init() { + if File_ContentType_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_ContentType_proto_rawDesc, + NumEnums: 1, + NumMessages: 0, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_ContentType_proto_goTypes, + DependencyIndexes: file_ContentType_proto_depIdxs, + EnumInfos: file_ContentType_proto_enumTypes, + }.Build() + File_ContentType_proto = out.File + file_ContentType_proto_rawDesc = nil + file_ContentType_proto_goTypes = nil + file_ContentType_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/List.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/List.pb.go new file mode 100644 index 0000000..ec77e02 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/List.pb.go @@ -0,0 +1,306 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: List.proto + +package yotiprotoattr + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AttributeAndId struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Attribute *Attribute `protobuf:"bytes,1,opt,name=attribute,proto3" json:"attribute,omitempty"` + AttributeId []byte `protobuf:"bytes,2,opt,name=attribute_id,json=attributeId,proto3" json:"attribute_id,omitempty"` +} + +func (x *AttributeAndId) Reset() { + *x = AttributeAndId{} + if protoimpl.UnsafeEnabled { + mi := &file_List_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AttributeAndId) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributeAndId) ProtoMessage() {} + +func (x *AttributeAndId) ProtoReflect() protoreflect.Message { + mi := &file_List_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributeAndId.ProtoReflect.Descriptor instead. +func (*AttributeAndId) Descriptor() ([]byte, []int) { + return file_List_proto_rawDescGZIP(), []int{0} +} + +func (x *AttributeAndId) GetAttribute() *Attribute { + if x != nil { + return x.Attribute + } + return nil +} + +func (x *AttributeAndId) GetAttributeId() []byte { + if x != nil { + return x.AttributeId + } + return nil +} + +type AttributeAndIdList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AttributeAndIdList []*AttributeAndId `protobuf:"bytes,1,rep,name=attribute_and_id_list,json=attributeAndIdList,proto3" json:"attribute_and_id_list,omitempty"` +} + +func (x *AttributeAndIdList) Reset() { + *x = AttributeAndIdList{} + if protoimpl.UnsafeEnabled { + mi := &file_List_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AttributeAndIdList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributeAndIdList) ProtoMessage() {} + +func (x *AttributeAndIdList) ProtoReflect() protoreflect.Message { + mi := &file_List_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributeAndIdList.ProtoReflect.Descriptor instead. +func (*AttributeAndIdList) Descriptor() ([]byte, []int) { + return file_List_proto_rawDescGZIP(), []int{1} +} + +func (x *AttributeAndIdList) GetAttributeAndIdList() []*AttributeAndId { + if x != nil { + return x.AttributeAndIdList + } + return nil +} + +type AttributeList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Attributes []*Attribute `protobuf:"bytes,1,rep,name=attributes,proto3" json:"attributes,omitempty"` +} + +func (x *AttributeList) Reset() { + *x = AttributeList{} + if protoimpl.UnsafeEnabled { + mi := &file_List_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AttributeList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributeList) ProtoMessage() {} + +func (x *AttributeList) ProtoReflect() protoreflect.Message { + mi := &file_List_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributeList.ProtoReflect.Descriptor instead. +func (*AttributeList) Descriptor() ([]byte, []int) { + return file_List_proto_rawDescGZIP(), []int{2} +} + +func (x *AttributeList) GetAttributes() []*Attribute { + if x != nil { + return x.Attributes + } + return nil +} + +var File_List_proto protoreflect.FileDescriptor + +var file_List_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x61, 0x74, + 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x1a, 0x0f, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6b, 0x0a, 0x0e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x41, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x36, + 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, + 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x22, 0x66, 0x0a, 0x12, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x41, 0x6e, 0x64, 0x49, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, + 0x50, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x61, 0x6e, 0x64, + 0x5f, 0x69, 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x41, 0x6e, 0x64, 0x49, 0x64, 0x52, 0x12, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x41, 0x6e, 0x64, 0x49, 0x64, 0x4c, 0x69, 0x73, + 0x74, 0x22, 0x49, 0x0a, 0x0d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4c, 0x69, + 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, + 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0xe9, 0x01, 0x0a, + 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, 0x74, 0x69, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x12, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, + 0x74, 0x69, 0x2d, 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, + 0x69, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x74, 0x74, 0x72, 0xaa, 0x02, 0x1c, 0x59, 0x6f, 0x74, + 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0xca, 0x02, 0x18, 0x59, 0x6f, 0x74, 0x69, + 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x70, 0x75, + 0x62, 0x61, 0x70, 0x69, 0xe2, 0x02, 0x24, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, + 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1a, 0x59, 0x6f, + 0x74, 0x69, 0x3a, 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3a, 0x3a, 0x41, 0x74, + 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_List_proto_rawDescOnce sync.Once + file_List_proto_rawDescData = file_List_proto_rawDesc +) + +func file_List_proto_rawDescGZIP() []byte { + file_List_proto_rawDescOnce.Do(func() { + file_List_proto_rawDescData = protoimpl.X.CompressGZIP(file_List_proto_rawDescData) + }) + return file_List_proto_rawDescData +} + +var file_List_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_List_proto_goTypes = []interface{}{ + (*AttributeAndId)(nil), // 0: attrpubapi_v1.AttributeAndId + (*AttributeAndIdList)(nil), // 1: attrpubapi_v1.AttributeAndIdList + (*AttributeList)(nil), // 2: attrpubapi_v1.AttributeList + (*Attribute)(nil), // 3: attrpubapi_v1.Attribute +} +var file_List_proto_depIdxs = []int32{ + 3, // 0: attrpubapi_v1.AttributeAndId.attribute:type_name -> attrpubapi_v1.Attribute + 0, // 1: attrpubapi_v1.AttributeAndIdList.attribute_and_id_list:type_name -> attrpubapi_v1.AttributeAndId + 3, // 2: attrpubapi_v1.AttributeList.attributes:type_name -> attrpubapi_v1.Attribute + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_List_proto_init() } +func file_List_proto_init() { + if File_List_proto != nil { + return + } + file_Attribute_proto_init() + if !protoimpl.UnsafeEnabled { + file_List_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AttributeAndId); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_List_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AttributeAndIdList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_List_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AttributeList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_List_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_List_proto_goTypes, + DependencyIndexes: file_List_proto_depIdxs, + MessageInfos: file_List_proto_msgTypes, + }.Build() + File_List_proto = out.File + file_List_proto_rawDesc = nil + file_List_proto_goTypes = nil + file_List_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/Signing.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/Signing.pb.go new file mode 100644 index 0000000..91e8609 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoattr/Signing.pb.go @@ -0,0 +1,224 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: Signing.proto + +package yotiprotoattr + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AttributeSigning struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + ContentType ContentType `protobuf:"varint,3,opt,name=content_type,json=contentType,proto3,enum=attrpubapi_v1.ContentType" json:"content_type,omitempty"` + ArtifactSignature []byte `protobuf:"bytes,4,opt,name=artifact_signature,json=artifactSignature,proto3" json:"artifact_signature,omitempty"` + SubType string `protobuf:"bytes,5,opt,name=sub_type,json=subType,proto3" json:"sub_type,omitempty"` + SignedTimeStamp []byte `protobuf:"bytes,6,opt,name=signed_time_stamp,json=signedTimeStamp,proto3" json:"signed_time_stamp,omitempty"` + AssociatedSource string `protobuf:"bytes,7,opt,name=associated_source,json=associatedSource,proto3" json:"associated_source,omitempty"` +} + +func (x *AttributeSigning) Reset() { + *x = AttributeSigning{} + if protoimpl.UnsafeEnabled { + mi := &file_Signing_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AttributeSigning) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributeSigning) ProtoMessage() {} + +func (x *AttributeSigning) ProtoReflect() protoreflect.Message { + mi := &file_Signing_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributeSigning.ProtoReflect.Descriptor instead. +func (*AttributeSigning) Descriptor() ([]byte, []int) { + return file_Signing_proto_rawDescGZIP(), []int{0} +} + +func (x *AttributeSigning) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AttributeSigning) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *AttributeSigning) GetContentType() ContentType { + if x != nil { + return x.ContentType + } + return ContentType_UNDEFINED +} + +func (x *AttributeSigning) GetArtifactSignature() []byte { + if x != nil { + return x.ArtifactSignature + } + return nil +} + +func (x *AttributeSigning) GetSubType() string { + if x != nil { + return x.SubType + } + return "" +} + +func (x *AttributeSigning) GetSignedTimeStamp() []byte { + if x != nil { + return x.SignedTimeStamp + } + return nil +} + +func (x *AttributeSigning) GetAssociatedSource() string { + if x != nil { + return x.AssociatedSource + } + return "" +} + +var File_Signing_proto protoreflect.FileDescriptor + +var file_Signing_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x0d, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x1a, 0x11, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x9e, 0x02, 0x0a, 0x10, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x53, + 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x61, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, + 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x2d, 0x0a, 0x12, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x61, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x19, + 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x73, 0x75, 0x62, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, + 0x53, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x42, 0xe3, 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, 0x74, 0x69, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, 0x69, 0x2e, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x0c, 0x53, 0x69, 0x67, + 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, + 0x74, 0x69, 0x2d, 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, + 0x69, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x61, 0x74, 0x74, 0x72, 0xaa, 0x02, 0x1c, 0x59, 0x6f, 0x74, + 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0xca, 0x02, 0x18, 0x59, 0x6f, 0x74, 0x69, + 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x70, 0x75, + 0x62, 0x61, 0x70, 0x69, 0xe2, 0x02, 0x24, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x41, 0x74, 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, + 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1a, 0x59, 0x6f, + 0x74, 0x69, 0x3a, 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3a, 0x3a, 0x41, 0x74, + 0x74, 0x72, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_Signing_proto_rawDescOnce sync.Once + file_Signing_proto_rawDescData = file_Signing_proto_rawDesc +) + +func file_Signing_proto_rawDescGZIP() []byte { + file_Signing_proto_rawDescOnce.Do(func() { + file_Signing_proto_rawDescData = protoimpl.X.CompressGZIP(file_Signing_proto_rawDescData) + }) + return file_Signing_proto_rawDescData +} + +var file_Signing_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_Signing_proto_goTypes = []interface{}{ + (*AttributeSigning)(nil), // 0: attrpubapi_v1.AttributeSigning + (ContentType)(0), // 1: attrpubapi_v1.ContentType +} +var file_Signing_proto_depIdxs = []int32{ + 1, // 0: attrpubapi_v1.AttributeSigning.content_type:type_name -> attrpubapi_v1.ContentType + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_Signing_proto_init() } +func file_Signing_proto_init() { + if File_Signing_proto != nil { + return + } + file_ContentType_proto_init() + if !protoimpl.UnsafeEnabled { + file_Signing_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AttributeSigning); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_Signing_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_Signing_proto_goTypes, + DependencyIndexes: file_Signing_proto_depIdxs, + MessageInfos: file_Signing_proto_msgTypes, + }.Build() + File_Signing_proto = out.File + file_Signing_proto_rawDesc = nil + file_Signing_proto_goTypes = nil + file_Signing_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotocom/EncryptedData.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotocom/EncryptedData.pb.go new file mode 100644 index 0000000..9bb7f54 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotocom/EncryptedData.pb.go @@ -0,0 +1,167 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: EncryptedData.proto + +package yotiprotocom + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EncryptedData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Iv []byte `protobuf:"bytes,1,opt,name=iv,proto3" json:"iv,omitempty"` + CipherText []byte `protobuf:"bytes,2,opt,name=cipher_text,json=cipherText,proto3" json:"cipher_text,omitempty"` +} + +func (x *EncryptedData) Reset() { + *x = EncryptedData{} + if protoimpl.UnsafeEnabled { + mi := &file_EncryptedData_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EncryptedData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EncryptedData) ProtoMessage() {} + +func (x *EncryptedData) ProtoReflect() protoreflect.Message { + mi := &file_EncryptedData_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EncryptedData.ProtoReflect.Descriptor instead. +func (*EncryptedData) Descriptor() ([]byte, []int) { + return file_EncryptedData_proto_rawDescGZIP(), []int{0} +} + +func (x *EncryptedData) GetIv() []byte { + if x != nil { + return x.Iv + } + return nil +} + +func (x *EncryptedData) GetCipherText() []byte { + if x != nil { + return x.CipherText + } + return nil +} + +var File_EncryptedData_proto protoreflect.FileDescriptor + +var file_EncryptedData_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, + 0x5f, 0x76, 0x31, 0x22, 0x40, 0x0a, 0x0d, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x44, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x76, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x02, 0x69, 0x76, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x5f, 0x74, + 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, + 0x72, 0x54, 0x65, 0x78, 0x74, 0x42, 0xe2, 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, + 0x74, 0x69, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, + 0x69, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x12, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x2d, 0x67, 0x6f, 0x2d, 0x73, + 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6d, 0xaa, 0x02, 0x19, 0x59, 0x6f, 0x74, 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0xca, 0x02, + 0x17, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x43, + 0x6f, 0x6d, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0xe2, 0x02, 0x23, 0x59, 0x6f, 0x74, 0x69, 0x5c, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x62, 0x61, + 0x70, 0x69, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x19, 0x59, 0x6f, 0x74, 0x69, 0x3a, 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3a, + 0x3a, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_EncryptedData_proto_rawDescOnce sync.Once + file_EncryptedData_proto_rawDescData = file_EncryptedData_proto_rawDesc +) + +func file_EncryptedData_proto_rawDescGZIP() []byte { + file_EncryptedData_proto_rawDescOnce.Do(func() { + file_EncryptedData_proto_rawDescData = protoimpl.X.CompressGZIP(file_EncryptedData_proto_rawDescData) + }) + return file_EncryptedData_proto_rawDescData +} + +var file_EncryptedData_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_EncryptedData_proto_goTypes = []interface{}{ + (*EncryptedData)(nil), // 0: compubapi_v1.EncryptedData +} +var file_EncryptedData_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_EncryptedData_proto_init() } +func file_EncryptedData_proto_init() { + if File_EncryptedData_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_EncryptedData_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EncryptedData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_EncryptedData_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_EncryptedData_proto_goTypes, + DependencyIndexes: file_EncryptedData_proto_depIdxs, + MessageInfos: file_EncryptedData_proto_msgTypes, + }.Build() + File_EncryptedData_proto = out.File + file_EncryptedData_proto_rawDesc = nil + file_EncryptedData_proto_goTypes = nil + file_EncryptedData_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotocom/SignedTimestamp.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotocom/SignedTimestamp.pb.go new file mode 100644 index 0000000..3f0b13d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotocom/SignedTimestamp.pb.go @@ -0,0 +1,210 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: SignedTimestamp.proto + +package yotiprotocom + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignedTimestamp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Timestamp uint64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + MessageDigest []byte `protobuf:"bytes,3,opt,name=message_digest,json=messageDigest,proto3" json:"message_digest,omitempty"` + ChainDigest []byte `protobuf:"bytes,4,opt,name=chain_digest,json=chainDigest,proto3" json:"chain_digest,omitempty"` + ChainDigestSkip1 []byte `protobuf:"bytes,5,opt,name=chain_digest_skip1,json=chainDigestSkip1,proto3" json:"chain_digest_skip1,omitempty"` + ChainDigestSkip2 []byte `protobuf:"bytes,6,opt,name=chain_digest_skip2,json=chainDigestSkip2,proto3" json:"chain_digest_skip2,omitempty"` +} + +func (x *SignedTimestamp) Reset() { + *x = SignedTimestamp{} + if protoimpl.UnsafeEnabled { + mi := &file_SignedTimestamp_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignedTimestamp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignedTimestamp) ProtoMessage() {} + +func (x *SignedTimestamp) ProtoReflect() protoreflect.Message { + mi := &file_SignedTimestamp_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignedTimestamp.ProtoReflect.Descriptor instead. +func (*SignedTimestamp) Descriptor() ([]byte, []int) { + return file_SignedTimestamp_proto_rawDescGZIP(), []int{0} +} + +func (x *SignedTimestamp) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *SignedTimestamp) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *SignedTimestamp) GetMessageDigest() []byte { + if x != nil { + return x.MessageDigest + } + return nil +} + +func (x *SignedTimestamp) GetChainDigest() []byte { + if x != nil { + return x.ChainDigest + } + return nil +} + +func (x *SignedTimestamp) GetChainDigestSkip1() []byte { + if x != nil { + return x.ChainDigestSkip1 + } + return nil +} + +func (x *SignedTimestamp) GetChainDigestSkip2() []byte { + if x != nil { + return x.ChainDigestSkip2 + } + return nil +} + +var File_SignedTimestamp_proto protoreflect.FileDescriptor + +var file_SignedTimestamp_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x62, 0x61, + 0x70, 0x69, 0x5f, 0x76, 0x31, 0x22, 0xef, 0x01, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x64, 0x69, 0x67, + 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, + 0x63, 0x68, 0x61, 0x69, 0x6e, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x6b, 0x69, 0x70, + 0x31, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x44, 0x69, + 0x67, 0x65, 0x73, 0x74, 0x53, 0x6b, 0x69, 0x70, 0x31, 0x12, 0x2c, 0x0a, 0x12, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x6b, 0x69, 0x70, 0x32, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x44, 0x69, 0x67, 0x65, + 0x73, 0x74, 0x53, 0x6b, 0x69, 0x70, 0x32, 0x42, 0xe4, 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, + 0x79, 0x6f, 0x74, 0x69, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, + 0x73, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x42, 0x14, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x2d, + 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0xaa, 0x02, 0x19, 0x59, 0x6f, 0x74, 0x69, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0xca, 0x02, 0x17, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x5c, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0xe2, 0x02, 0x23, 0x59, + 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x43, 0x6f, 0x6d, + 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0xea, 0x02, 0x19, 0x59, 0x6f, 0x74, 0x69, 0x3a, 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x3a, 0x3a, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_SignedTimestamp_proto_rawDescOnce sync.Once + file_SignedTimestamp_proto_rawDescData = file_SignedTimestamp_proto_rawDesc +) + +func file_SignedTimestamp_proto_rawDescGZIP() []byte { + file_SignedTimestamp_proto_rawDescOnce.Do(func() { + file_SignedTimestamp_proto_rawDescData = protoimpl.X.CompressGZIP(file_SignedTimestamp_proto_rawDescData) + }) + return file_SignedTimestamp_proto_rawDescData +} + +var file_SignedTimestamp_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_SignedTimestamp_proto_goTypes = []interface{}{ + (*SignedTimestamp)(nil), // 0: compubapi_v1.SignedTimestamp +} +var file_SignedTimestamp_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_SignedTimestamp_proto_init() } +func file_SignedTimestamp_proto_init() { + if File_SignedTimestamp_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_SignedTimestamp_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignedTimestamp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_SignedTimestamp_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_SignedTimestamp_proto_goTypes, + DependencyIndexes: file_SignedTimestamp_proto_depIdxs, + MessageInfos: file_SignedTimestamp_proto_msgTypes, + }.Build() + File_SignedTimestamp_proto = out.File + file_SignedTimestamp_proto_rawDesc = nil + file_SignedTimestamp_proto_goTypes = nil + file_SignedTimestamp_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/DataEntry.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/DataEntry.pb.go new file mode 100644 index 0000000..add01e4 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/DataEntry.pb.go @@ -0,0 +1,242 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: DataEntry.proto + +package yotiprotoshare + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DataEntry_Type int32 + +const ( + DataEntry_UNDEFINED DataEntry_Type = 0 + DataEntry_INVOICE DataEntry_Type = 1 + DataEntry_PAYMENT_TRANSACTION DataEntry_Type = 2 + DataEntry_LOCATION DataEntry_Type = 3 + DataEntry_TRANSACTION DataEntry_Type = 4 + DataEntry_AGE_VERIFICATION_SECRET DataEntry_Type = 5 + DataEntry_THIRD_PARTY_ATTRIBUTE DataEntry_Type = 6 +) + +// Enum value maps for DataEntry_Type. +var ( + DataEntry_Type_name = map[int32]string{ + 0: "UNDEFINED", + 1: "INVOICE", + 2: "PAYMENT_TRANSACTION", + 3: "LOCATION", + 4: "TRANSACTION", + 5: "AGE_VERIFICATION_SECRET", + 6: "THIRD_PARTY_ATTRIBUTE", + } + DataEntry_Type_value = map[string]int32{ + "UNDEFINED": 0, + "INVOICE": 1, + "PAYMENT_TRANSACTION": 2, + "LOCATION": 3, + "TRANSACTION": 4, + "AGE_VERIFICATION_SECRET": 5, + "THIRD_PARTY_ATTRIBUTE": 6, + } +) + +func (x DataEntry_Type) Enum() *DataEntry_Type { + p := new(DataEntry_Type) + *p = x + return p +} + +func (x DataEntry_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DataEntry_Type) Descriptor() protoreflect.EnumDescriptor { + return file_DataEntry_proto_enumTypes[0].Descriptor() +} + +func (DataEntry_Type) Type() protoreflect.EnumType { + return &file_DataEntry_proto_enumTypes[0] +} + +func (x DataEntry_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DataEntry_Type.Descriptor instead. +func (DataEntry_Type) EnumDescriptor() ([]byte, []int) { + return file_DataEntry_proto_rawDescGZIP(), []int{0, 0} +} + +type DataEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type DataEntry_Type `protobuf:"varint,1,opt,name=type,proto3,enum=sharepubapi_v1.DataEntry_Type" json:"type,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *DataEntry) Reset() { + *x = DataEntry{} + if protoimpl.UnsafeEnabled { + mi := &file_DataEntry_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataEntry) ProtoMessage() {} + +func (x *DataEntry) ProtoReflect() protoreflect.Message { + mi := &file_DataEntry_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataEntry.ProtoReflect.Descriptor instead. +func (*DataEntry) Descriptor() ([]byte, []int) { + return file_DataEntry_proto_rawDescGZIP(), []int{0} +} + +func (x *DataEntry) GetType() DataEntry_Type { + if x != nil { + return x.Type + } + return DataEntry_UNDEFINED +} + +func (x *DataEntry) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +var File_DataEntry_proto protoreflect.FileDescriptor + +var file_DataEntry_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x0e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, + 0x31, 0x22, 0xea, 0x01, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x32, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, + 0x73, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x44, + 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x92, 0x01, 0x0a, 0x04, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, 0x01, 0x12, 0x17, + 0x0a, 0x13, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x41, + 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x43, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x41, 0x43, + 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x04, 0x12, 0x1b, 0x0a, 0x17, 0x41, 0x47, 0x45, 0x5f, 0x56, 0x45, + 0x52, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x45, 0x43, 0x52, 0x45, + 0x54, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x54, 0x48, 0x49, 0x52, 0x44, 0x5f, 0x50, 0x41, 0x52, + 0x54, 0x59, 0x5f, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x10, 0x06, 0x42, 0xe5, + 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, 0x74, 0x69, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x0e, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, 0x74, 0x69, + 0x2d, 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x61, 0x72, 0x65, 0xaa, 0x02, 0x18, 0x59, 0x6f, 0x74, 0x69, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, 0x53, + 0x68, 0x61, 0x72, 0x65, 0xca, 0x02, 0x19, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, + 0xe2, 0x02, 0x25, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x5c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x59, 0x6f, 0x74, 0x69, 0x3a, + 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3a, 0x3a, 0x53, 0x68, 0x61, 0x72, 0x65, + 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_DataEntry_proto_rawDescOnce sync.Once + file_DataEntry_proto_rawDescData = file_DataEntry_proto_rawDesc +) + +func file_DataEntry_proto_rawDescGZIP() []byte { + file_DataEntry_proto_rawDescOnce.Do(func() { + file_DataEntry_proto_rawDescData = protoimpl.X.CompressGZIP(file_DataEntry_proto_rawDescData) + }) + return file_DataEntry_proto_rawDescData +} + +var file_DataEntry_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_DataEntry_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_DataEntry_proto_goTypes = []interface{}{ + (DataEntry_Type)(0), // 0: sharepubapi_v1.DataEntry.Type + (*DataEntry)(nil), // 1: sharepubapi_v1.DataEntry +} +var file_DataEntry_proto_depIdxs = []int32{ + 0, // 0: sharepubapi_v1.DataEntry.type:type_name -> sharepubapi_v1.DataEntry.Type + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_DataEntry_proto_init() } +func file_DataEntry_proto_init() { + if File_DataEntry_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_DataEntry_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataEntry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_DataEntry_proto_rawDesc, + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_DataEntry_proto_goTypes, + DependencyIndexes: file_DataEntry_proto_depIdxs, + EnumInfos: file_DataEntry_proto_enumTypes, + MessageInfos: file_DataEntry_proto_msgTypes, + }.Build() + File_DataEntry_proto = out.File + file_DataEntry_proto_rawDesc = nil + file_DataEntry_proto_goTypes = nil + file_DataEntry_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/ExtraData.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/ExtraData.pb.go new file mode 100644 index 0000000..88e555f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/ExtraData.pb.go @@ -0,0 +1,162 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: ExtraData.proto + +package yotiprotoshare + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ExtraData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + List []*DataEntry `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` +} + +func (x *ExtraData) Reset() { + *x = ExtraData{} + if protoimpl.UnsafeEnabled { + mi := &file_ExtraData_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExtraData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExtraData) ProtoMessage() {} + +func (x *ExtraData) ProtoReflect() protoreflect.Message { + mi := &file_ExtraData_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExtraData.ProtoReflect.Descriptor instead. +func (*ExtraData) Descriptor() ([]byte, []int) { + return file_ExtraData_proto_rawDescGZIP(), []int{0} +} + +func (x *ExtraData) GetList() []*DataEntry { + if x != nil { + return x.List + } + return nil +} + +var File_ExtraData_proto protoreflect.FileDescriptor + +var file_ExtraData_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x45, 0x78, 0x74, 0x72, 0x61, 0x44, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x0e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, + 0x31, 0x1a, 0x0f, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0x3a, 0x0a, 0x09, 0x45, 0x78, 0x74, 0x72, 0x61, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x2d, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x73, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x44, + 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x42, 0xe5, + 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, 0x74, 0x69, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x0e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x44, 0x61, + 0x74, 0x61, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, 0x74, 0x69, + 0x2d, 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x61, 0x72, 0x65, 0xaa, 0x02, 0x18, 0x59, 0x6f, 0x74, 0x69, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, 0x53, + 0x68, 0x61, 0x72, 0x65, 0xca, 0x02, 0x19, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, + 0xe2, 0x02, 0x25, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x5c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x59, 0x6f, 0x74, 0x69, 0x3a, + 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3a, 0x3a, 0x53, 0x68, 0x61, 0x72, 0x65, + 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_ExtraData_proto_rawDescOnce sync.Once + file_ExtraData_proto_rawDescData = file_ExtraData_proto_rawDesc +) + +func file_ExtraData_proto_rawDescGZIP() []byte { + file_ExtraData_proto_rawDescOnce.Do(func() { + file_ExtraData_proto_rawDescData = protoimpl.X.CompressGZIP(file_ExtraData_proto_rawDescData) + }) + return file_ExtraData_proto_rawDescData +} + +var file_ExtraData_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_ExtraData_proto_goTypes = []interface{}{ + (*ExtraData)(nil), // 0: sharepubapi_v1.ExtraData + (*DataEntry)(nil), // 1: sharepubapi_v1.DataEntry +} +var file_ExtraData_proto_depIdxs = []int32{ + 1, // 0: sharepubapi_v1.ExtraData.list:type_name -> sharepubapi_v1.DataEntry + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_ExtraData_proto_init() } +func file_ExtraData_proto_init() { + if File_ExtraData_proto != nil { + return + } + file_DataEntry_proto_init() + if !protoimpl.UnsafeEnabled { + file_ExtraData_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExtraData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_ExtraData_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_ExtraData_proto_goTypes, + DependencyIndexes: file_ExtraData_proto_depIdxs, + MessageInfos: file_ExtraData_proto_msgTypes, + }.Build() + File_ExtraData_proto = out.File + file_ExtraData_proto_rawDesc = nil + file_ExtraData_proto_goTypes = nil + file_ExtraData_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/IssuingAttributes.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/IssuingAttributes.pb.go new file mode 100644 index 0000000..758fb28 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/IssuingAttributes.pb.go @@ -0,0 +1,234 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: IssuingAttributes.proto + +package yotiprotoshare + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type IssuingAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ExpiryDate string `protobuf:"bytes,1,opt,name=expiry_date,json=expiryDate,proto3" json:"expiry_date,omitempty"` + Definitions []*Definition `protobuf:"bytes,2,rep,name=definitions,proto3" json:"definitions,omitempty"` +} + +func (x *IssuingAttributes) Reset() { + *x = IssuingAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_IssuingAttributes_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IssuingAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IssuingAttributes) ProtoMessage() {} + +func (x *IssuingAttributes) ProtoReflect() protoreflect.Message { + mi := &file_IssuingAttributes_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IssuingAttributes.ProtoReflect.Descriptor instead. +func (*IssuingAttributes) Descriptor() ([]byte, []int) { + return file_IssuingAttributes_proto_rawDescGZIP(), []int{0} +} + +func (x *IssuingAttributes) GetExpiryDate() string { + if x != nil { + return x.ExpiryDate + } + return "" +} + +func (x *IssuingAttributes) GetDefinitions() []*Definition { + if x != nil { + return x.Definitions + } + return nil +} + +type Definition struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *Definition) Reset() { + *x = Definition{} + if protoimpl.UnsafeEnabled { + mi := &file_IssuingAttributes_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Definition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Definition) ProtoMessage() {} + +func (x *Definition) ProtoReflect() protoreflect.Message { + mi := &file_IssuingAttributes_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Definition.ProtoReflect.Descriptor instead. +func (*Definition) Descriptor() ([]byte, []int) { + return file_IssuingAttributes_proto_rawDescGZIP(), []int{1} +} + +func (x *Definition) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_IssuingAttributes_proto protoreflect.FileDescriptor + +var file_IssuingAttributes_proto_rawDesc = []byte{ + 0x0a, 0x17, 0x49, 0x73, 0x73, 0x75, 0x69, 0x6e, 0x67, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x73, 0x68, 0x61, 0x72, 0x65, + 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x22, 0x72, 0x0a, 0x11, 0x49, 0x73, 0x73, + 0x75, 0x69, 0x6e, 0x67, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1f, + 0x0a, 0x0b, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x44, 0x61, 0x74, 0x65, 0x12, + 0x3c, 0x0a, 0x0b, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, + 0x70, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x0b, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x20, 0x0a, + 0x0a, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, + 0xed, 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, 0x74, 0x69, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x6d, 0x6f, + 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x16, 0x49, 0x73, 0x73, 0x75, 0x69, 0x6e, + 0x67, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, + 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x2d, 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, + 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, 0x69, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x61, + 0x72, 0x65, 0xaa, 0x02, 0x18, 0x59, 0x6f, 0x74, 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0xca, 0x02, 0x19, + 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x53, 0x68, + 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0xe2, 0x02, 0x25, 0x59, 0x6f, 0x74, 0x69, + 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, + 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0xea, 0x02, 0x1b, 0x59, 0x6f, 0x74, 0x69, 0x3a, 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x3a, 0x3a, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_IssuingAttributes_proto_rawDescOnce sync.Once + file_IssuingAttributes_proto_rawDescData = file_IssuingAttributes_proto_rawDesc +) + +func file_IssuingAttributes_proto_rawDescGZIP() []byte { + file_IssuingAttributes_proto_rawDescOnce.Do(func() { + file_IssuingAttributes_proto_rawDescData = protoimpl.X.CompressGZIP(file_IssuingAttributes_proto_rawDescData) + }) + return file_IssuingAttributes_proto_rawDescData +} + +var file_IssuingAttributes_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_IssuingAttributes_proto_goTypes = []interface{}{ + (*IssuingAttributes)(nil), // 0: sharepubapi_v1.IssuingAttributes + (*Definition)(nil), // 1: sharepubapi_v1.Definition +} +var file_IssuingAttributes_proto_depIdxs = []int32{ + 1, // 0: sharepubapi_v1.IssuingAttributes.definitions:type_name -> sharepubapi_v1.Definition + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_IssuingAttributes_proto_init() } +func file_IssuingAttributes_proto_init() { + if File_IssuingAttributes_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_IssuingAttributes_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IssuingAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_IssuingAttributes_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Definition); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_IssuingAttributes_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_IssuingAttributes_proto_goTypes, + DependencyIndexes: file_IssuingAttributes_proto_depIdxs, + MessageInfos: file_IssuingAttributes_proto_msgTypes, + }.Build() + File_IssuingAttributes_proto = out.File + file_IssuingAttributes_proto_rawDesc = nil + file_IssuingAttributes_proto_goTypes = nil + file_IssuingAttributes_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/ThirdPartyAttribute.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/ThirdPartyAttribute.pb.go new file mode 100644 index 0000000..11623d2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk/v3@v3.14.0/yotiprotoshare/ThirdPartyAttribute.pb.go @@ -0,0 +1,177 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.1 +// source: ThirdPartyAttribute.proto + +package yotiprotoshare + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ThirdPartyAttribute struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IssuanceToken []byte `protobuf:"bytes,1,opt,name=issuance_token,json=issuanceToken,proto3" json:"issuance_token,omitempty"` + IssuingAttributes *IssuingAttributes `protobuf:"bytes,2,opt,name=issuing_attributes,json=issuingAttributes,proto3" json:"issuing_attributes,omitempty"` +} + +func (x *ThirdPartyAttribute) Reset() { + *x = ThirdPartyAttribute{} + if protoimpl.UnsafeEnabled { + mi := &file_ThirdPartyAttribute_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ThirdPartyAttribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThirdPartyAttribute) ProtoMessage() {} + +func (x *ThirdPartyAttribute) ProtoReflect() protoreflect.Message { + mi := &file_ThirdPartyAttribute_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThirdPartyAttribute.ProtoReflect.Descriptor instead. +func (*ThirdPartyAttribute) Descriptor() ([]byte, []int) { + return file_ThirdPartyAttribute_proto_rawDescGZIP(), []int{0} +} + +func (x *ThirdPartyAttribute) GetIssuanceToken() []byte { + if x != nil { + return x.IssuanceToken + } + return nil +} + +func (x *ThirdPartyAttribute) GetIssuingAttributes() *IssuingAttributes { + if x != nil { + return x.IssuingAttributes + } + return nil +} + +var File_ThirdPartyAttribute_proto protoreflect.FileDescriptor + +var file_ThirdPartyAttribute_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x73, 0x68, 0x61, + 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, 0x1a, 0x17, 0x49, 0x73, 0x73, + 0x75, 0x69, 0x6e, 0x67, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8e, 0x01, 0x0a, 0x13, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, + 0x72, 0x74, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, + 0x69, 0x73, 0x73, 0x75, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x69, 0x73, 0x73, 0x75, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x12, 0x69, 0x73, 0x73, 0x75, 0x69, 0x6e, 0x67, 0x5f, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x21, 0x2e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x31, + 0x2e, 0x49, 0x73, 0x73, 0x75, 0x69, 0x6e, 0x67, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x52, 0x11, 0x69, 0x73, 0x73, 0x75, 0x69, 0x6e, 0x67, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0xef, 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x79, 0x6f, + 0x74, 0x69, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x70, + 0x69, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x18, + 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x65, 0x74, 0x79, 0x6f, 0x74, 0x69, 0x2f, 0x79, 0x6f, 0x74, + 0x69, 0x2d, 0x67, 0x6f, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x79, 0x6f, 0x74, 0x69, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x61, 0x72, 0x65, 0xaa, 0x02, 0x18, 0x59, 0x6f, 0x74, + 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x75, 0x66, 0x2e, + 0x53, 0x68, 0x61, 0x72, 0x65, 0xca, 0x02, 0x19, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x5c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, + 0x69, 0xe2, 0x02, 0x25, 0x59, 0x6f, 0x74, 0x69, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x5c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x5c, 0x47, 0x50, + 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1b, 0x59, 0x6f, 0x74, 0x69, + 0x3a, 0x3a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3a, 0x3a, 0x53, 0x68, 0x61, 0x72, + 0x65, 0x70, 0x75, 0x62, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_ThirdPartyAttribute_proto_rawDescOnce sync.Once + file_ThirdPartyAttribute_proto_rawDescData = file_ThirdPartyAttribute_proto_rawDesc +) + +func file_ThirdPartyAttribute_proto_rawDescGZIP() []byte { + file_ThirdPartyAttribute_proto_rawDescOnce.Do(func() { + file_ThirdPartyAttribute_proto_rawDescData = protoimpl.X.CompressGZIP(file_ThirdPartyAttribute_proto_rawDescData) + }) + return file_ThirdPartyAttribute_proto_rawDescData +} + +var file_ThirdPartyAttribute_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_ThirdPartyAttribute_proto_goTypes = []interface{}{ + (*ThirdPartyAttribute)(nil), // 0: sharepubapi_v1.ThirdPartyAttribute + (*IssuingAttributes)(nil), // 1: sharepubapi_v1.IssuingAttributes +} +var file_ThirdPartyAttribute_proto_depIdxs = []int32{ + 1, // 0: sharepubapi_v1.ThirdPartyAttribute.issuing_attributes:type_name -> sharepubapi_v1.IssuingAttributes + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_ThirdPartyAttribute_proto_init() } +func file_ThirdPartyAttribute_proto_init() { + if File_ThirdPartyAttribute_proto != nil { + return + } + file_IssuingAttributes_proto_init() + if !protoimpl.UnsafeEnabled { + file_ThirdPartyAttribute_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ThirdPartyAttribute); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_ThirdPartyAttribute_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_ThirdPartyAttribute_proto_goTypes, + DependencyIndexes: file_ThirdPartyAttribute_proto_depIdxs, + MessageInfos: file_ThirdPartyAttribute_proto_msgTypes, + }.Build() + File_ThirdPartyAttribute_proto = out.File + file_ThirdPartyAttribute_proto_rawDesc = nil + file_ThirdPartyAttribute_proto_goTypes = nil + file_ThirdPartyAttribute_proto_depIdxs = nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/.gitignore b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/.gitignore new file mode 100644 index 0000000..b79bd93 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/.gitignore @@ -0,0 +1,12 @@ +examples/profile/images/YotiSelfie.jpeg +.vscode + +# Test binary, build with `go test -c` +*.test + +# Example project generated self-signed certificate +examples/profile/yotiSelfSignedCert.pem +examples/profile/yotiSelfSignedKey.pem + +# Debug files +debug \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/CONTRIBUTING.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/CONTRIBUTING.md new file mode 100644 index 0000000..3ba94fb --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing + +The command `go get "github.com/getyoti/yoti-go-sdk"` downloads the Yoti package, along with its dependencies, and installs it. + +## Commit Process + +1) `goimports` formats the code and sanitises imports +1) `go vet` reports suspicious constructs +1) `go test` to run the tests + +## VS Code + +For developing in VS Code, use the following `launch.json` file (placed inside a `.vscode` folder) to easily run the examples from VS Code: + +```javascript +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "AML Example", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/examples/aml/main.go" + }, + { + "name": "Example", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "host": "127.0.0.1", + "program": "${workspaceFolder}/examples/profile/main.go", + "env": {}, + "args": ["certificatehelper.go"], + "showLog": true + } + ] +} +``` \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/LICENSE.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/LICENSE.md new file mode 100644 index 0000000..fefdd34 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/LICENSE.md @@ -0,0 +1,23 @@ +# MIT License + +Copyright © 2017 Yoti Ltd + +* * * + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/README.md b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/README.md new file mode 100644 index 0000000..f5d3580 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/README.md @@ -0,0 +1,339 @@ +# Yoti Go SDK + +Welcome to the Yoti Go SDK. This repo contains the tools and step by step instructions you need to quickly integrate your Go back-end with Yoti so that your users can share their identity details with your application in a secure and trusted way. + +## Table of Contents + +1) [An Architectural view](#an-architectural-view) - +High level overview of integration + +1) [Installing the SDK](#installing-the-sdk) - +How to install our SDK + +1) [SDK Project import](#sdk-project-import) - +How to install the SDK to your project + +1) [Configuration](#configuration) - +How to initialise your configuration + +1) [Profile Retrieval](#profile-retrieval) - +How to retrieve a Yoti profile using the one time use token + +1) [Handling users](#handling-users) - +How to manage users + +1) [AML Integration](#aml-integration) - +How to integrate with Yoti's AML (Anti Money Laundering) service + +1) [Running the tests](#running-the-tests) - +Attributes defined + +1) [Running the example](#running-the-profile-example) - +Attributes defined + +1) [API Coverage](#api-coverage) - +Attributes defined + +1) [Support](#support) - +Please feel free to reach out + +1) [References](#references) + +## An Architectural View + +Before you start your integration, here is a bit of background on how the integration works. To integrate your application with Yoti, your back-end must expose a GET endpoint that Yoti will use to forward tokens. +The endpoint is configured in the [Yoti Dashboard](https://www.yoti.com/dashboard) where you create/update your application. For more information on how to create an application please check our [developer page](https://www.yoti.com/developers/documentation/#login-button-setup). + +The image below shows how your application back-end and Yoti integrate into the context of a Login flow. +Yoti SDK carries out for you steps 6, 7 ,8 and the profile decryption in step 9. + +![alt text](login_flow.png "Login flow") + +Yoti also allows you to enable user details verification from your mobile app by means of the Android (TBA) and iOS (TBA) SDKs. In that scenario, your Yoti-enabled mobile app is playing both the role of the browser and the Yoti app. Your back-end doesn't need to handle these cases in a significantly different way. You might just decide to handle the `User-Agent` header in order to provide different responses for desktop and mobile clients. + +## Installing the SDK + +To download and install the Yoti SDK and its dependencies, simply run the following command from your terminal: + +```Go +go get "github.com/getyoti/yoti-go-sdk" +``` + +## SDK Project import + +You can reference the project URL by adding the following import: + +```Go +import "github.com/getyoti/yoti-go-sdk" +``` + +## Configuration + +The YotiClient is the SDK entry point. To initialise it you need include the following snippet inside your endpoint initialisation section: + +```Go +sdkID := "your-sdk-id"; +key, err := ioutil.ReadFile("path/to/your-application-pem-file.pem") +if err != nil { + // handle key load error +} + +client := yoti.Client{ + SdkID: sdkID, + Key: key} +``` + +Where: + +* `sdkID` is the SDK identifier generated by Yoti Dashboard in the Key tab when you create your app. Note this is not your Application Identifier which is needed by your client-side code. + +* `path/to/your-application-pem-file.pem` is the path to the application pem file. It can be downloaded from the Keys tab in the [Yoti Dashboard](https://www.yoti.com/dashboard/applications). + +Please do not open the pem file as this might corrupt the key and you will need to create a new application. + +Keeping your settings and access keys outside your repository is highly recommended. You can use gems like [godotenv](https://github.com/joho/godotenv) to manage environment variables more easily. + +## Profile Retrieval + +When your application receives a one time use token via the exposed endpoint (it will be assigned to a query string parameter named `token`), you can easily retrieve the activity details by adding the following to your endpoint handler: + +```Go +activityDetails, errStrings := client.GetActivityDetails(yotiOneTimeUseToken) +if len(errStrings) != 0 { + // handle unhappy path +} +``` + +### Profile + +You can then get the user profile from the activityDetails struct: + +```Go +var rememberMeID string = activityDetails.RememberMeID() +var userProfile yoti.Profile = activityDetails.UserProfile + +var selfie = userProfile.Selfie().Value() +var givenNames string = userProfile.GivenNames().Value() +var familyName string = userProfile.FamilyName().Value() +var fullName string = userProfile.FullName().Value() +var mobileNumber string = userProfile.MobileNumber().Value() +var emailAddress string = userProfile.EmailAddress().Value() +var address string = userProfile.Address().Value() +var gender string = userProfile.Gender().Value() +var nationality string = userProfile.Nationality().Value() +var dateOfBirth *time.Time +dobAttr, err := userProfile.DateOfBirth() +if err != nil { + //handle error +} else { + dateOfBirth = dobAttr.Value() +} +var structuredPostalAddress map[string]interface{} +structuredPostalAddressAttribute, err := userProfile.StructuredPostalAddress() +if err != nil { + //handle error +} else { + structuredPostalAddress := structuredPostalAddressAttribute.Value().(map[string]interface{}) +} +``` + +If you have chosen Verify Condition on the Yoti Dashboard with the age condition of "Over 18", you can retrieve the user information with the generic .GetAttribute method, which requires the result to be cast to the original type: + +```Go +userProfile.GetAttribute("age_over:18").Value().(string) +``` + +GetAttribute returns an interface, the value can be acquired through a type assertion. + +### Anchors, Sources and Verifiers + +An `Anchor` represents how a given Attribute has been _sourced_ or _verified_. These values are created and signed whenever a Profile Attribute is created, or verified with an external party. + +For example, an attribute value that was _sourced_ from a Passport might have the following values: + +`Anchor` property | Example value +-----|------ +type | SOURCE +value | PASSPORT +subType | OCR +signedTimestamp | 2017-10-31, 19:45:59.123789 + +Similarly, an attribute _verified_ against the data held by an external party will have an `Anchor` of type _VERIFIER_, naming the party that verified it. + +From each attribute you can retrieve the `Anchors`, and subsets `Sources` and `Verifiers` (all as `[]*anchor.Anchor`) as follows: + +```Go +givenNamesAnchors := userProfile.GivenNames().Anchors() +givenNamesSources := userProfile.GivenNames().Sources() +givenNamesVerifiers := userProfile.GivenNames().Verifiers() +``` + +You can also retrieve further properties from these respective anchors in the following way: + +```Go +var givenNamesFirstAnchor *anchor.Anchor = givenNamesAnchors[0] + +var anchorType anchor.Type = givenNamesFirstAnchor.Type() +var signedTimestamp *time.Time = givenNamesFirstAnchor.SignedTimestamp().Timestamp() +var subType string = givenNamesFirstAnchor.SubType() +var value []string = givenNamesFirstAnchor.Value() +``` + +## Handling Users + +When you retrieve the user profile, you receive a user ID generated by Yoti exclusively for your application. +This means that if the same individual logs into another app, Yoti will assign her/him a different ID. +You can use this ID to verify whether (for your application) the retrieved profile identifies a new or an existing user. +Here is an example of how this works: + +```Go +activityDetails, err := client.GetActivityDetails(yotiOneTimeUseToken) +if err == nil { + user := YourUserSearchFunction(activityDetails.RememberMeID()) + if user != nil { + // handle login + } else { + // handle registration + } +} else { + // handle unhappy path +} +``` + +Where `yourUserSearchFunction` is a piece of logic in your app that is supposed to find a user, given a RememberMeID. +No matter if the user is a new or an existing one, Yoti will always provide her/his profile, so you don't necessarily need to store it. + +The `profile` object provides a set of attributes corresponding to user attributes. Whether the attributes are present or not depends on the settings you have applied to your app on Yoti Dashboard. + +## Running the Tests + +You can run the unit tests for this project by executing the following commands inside the repository folder + +```Go +go get -t +go test +``` + +## AML Integration + +Yoti provides an AML (Anti Money Laundering) check service to allow a deeper KYC process to prevent fraud. This is a chargeable service, so please contact [sdksupport@yoti.com](mailto:sdksupport@yoti.com) for more information. + +Yoti will provide a boolean result on the following checks: + +* PEP list - Verify against Politically Exposed Persons list +* Fraud list - Verify against US Social Security Administration Fraud (SSN Fraud) list +* Watch list - Verify against watch lists from the Office of Foreign Assets Control + +To use this functionality you must ensure your application is assigned to your Organisation in the Yoti Dashboard - please see [here](https://www.yoti.com/developers/documentation/#1-creating-an-organisation) for further information. + +For the AML check you will need to provide the following: + +* Data provided by Yoti (please ensure you have selected the Given name(s) and Family name attributes from the Data tab in the Yoti Dashboard) + * Given name(s) + * Family name +* Data that must be collected from the user: + * Country of residence (must be an ISO 3166 3-letter code) + * Social Security Number (US citizens only) + * Postcode/Zip code (US citizens only) + +### Consent + +Performing an AML check on a person *requires* their consent. +**You must ensure you have user consent *before* using this service.** + +### Code Example + +Given a YotiClient initialised with your SDK ID and KeyPair (see [Client Initialisation](#client-initialisation)) performing an AML check is a straightforward case of providing basic profile data. + +```Go +givenNames := "Edward Richard George" +familyName := "Heath" + +amlAddress := yoti.AmlAddress{ + Country: "GBR"} + +amlProfile := yoti.AmlProfile{ + GivenNames: givenNames, + FamilyName: familyName, + Address: amlAddress} + +result, err := client.PerformAmlCheck(amlProfile) + +log.Printf( + "AML Result for %s %s:", + givenNames, + familyName) +log.Printf( + "On PEP list: %s", + strconv.FormatBool(result.OnPEPList)) +log.Printf( + "On Fraud list: %s", + strconv.FormatBool(result.OnFraudList)) +log.Printf( + "On Watch list: %s", + strconv.FormatBool(result.OnWatchList)) +} +``` + +Additionally, an [example AML application](/examples/aml/main.go) is provided in the examples folder. + +* Rename the [.env.example](examples/profile/.env.example) file to `.env` and fill in the required configuration values (mentioned in the [Configuration](#configuration) section) +* Change directory to the aml example folder: `cd examples/aml` +* Install the dependencies with `go get` +* Start the example with `go run main.go` + +## Running the Profile Example + +The profile retrieval example can be found in the [examples folder](examples). + +* Change directory to the profile example folder: `cd examples/profile` +* On the [Yoti Dashboard](https://www.yoti.com/dashboard/applications): + * Set the application domain of your app to `localhost:8080` + * Set the scenario callback URL to `/profile` +* Rename the [.env.example](examples/profile/.env.example) file to `.env` and fill in the required configuration values (mentioned in the [Configuration](#configuration) section) +* Install the dependencies with `go get` +* Start the server with `go run main.go certificatehelper.go` + +Visiting `https://localhost:8080/` should show a Yoti Connect button + +## API Coverage + +* [X] Activity Details + * [X] Remember Me ID `RememberMeID()` + * [X] User Profile `UserProfile` + * [X] Selfie `Selfie()` + * [X] Selfie Base64 URL `Selfie().Value().Base64URL()` + * [X] Given Names `GivenNames()` + * [X] Family Name `FamilyName()` + * [X] Full Name `FullName()` + * [X] Mobile Number `MobileNumber()` + * [X] Email Address `EmailAddress()` + * [X] Date of Birth `DateOfBirth()` + * [X] Postal Address `Address()` + * [X] Structured Postal Address `StructuredPostalAddress()` + * [X] Gender `Gender()` + * [X] Nationality `Nationality()` + +## Support + +For any questions or support please email [sdksupport@yoti.com](mailto:sdksupport@yoti.com). +Please provide the following to get you up and working as quickly as possible: + +* Computer type +* OS version +* Version of Go being used +* Screenshot + +Once we have answered your question we may contact you again to discuss Yoti products and services. If you’d prefer us not to do this, please let us know when you e-mail. + +## References + +* [AES-256 symmetric encryption][] +* [RSA pkcs asymmetric encryption][] +* [Protocol buffers][] +* [Base64 data][] + +[AES-256 symmetric encryption]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +[RSA pkcs asymmetric encryption]: https://en.wikipedia.org/wiki/RSA_(cryptosystem) +[Protocol buffers]: https://en.wikipedia.org/wiki/Protocol_Buffers +[Base64 data]: https://en.wikipedia.org/wiki/Base64 diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/activitydetails.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/activitydetails.go new file mode 100644 index 0000000..1758b6f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/activitydetails.go @@ -0,0 +1,14 @@ +package yoti + +// ActivityDetails represents the result of an activity between a user and the application. +type ActivityDetails struct { + UserProfile Profile + rememberMeID string +} + +// RememberMeID is a unique identifier Yoti assigns to your user, but only for your app. +// If the same user logs into your app again, you get the same id. +// If she/he logs into another application, Yoti will assign a different id for that app. +func (a ActivityDetails) RememberMeID() string { + return a.rememberMeID +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/activityerrors.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/activityerrors.go new file mode 100644 index 0000000..b26a1b7 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/activityerrors.go @@ -0,0 +1,12 @@ +package yoti + +import "errors" + +var ( + // ErrProfileNotFound profile was not found during activity retrieval for the provided one time use token + ErrProfileNotFound = errors.New("ProfileNotFound") + // ErrFailure there was a failure during activity retrieval + ErrFailure = errors.New("Failure") + // ErrSharingFailure there was a failure when sharing + ErrSharingFailure = errors.New("SharingFailure") +) diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/aml.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/aml.go new file mode 100644 index 0000000..544280e --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/aml.go @@ -0,0 +1,49 @@ +package yoti + +import ( + "encoding/json" + "log" +) + +// AmlAddress Address for Anti Money Laundering (AML) purposes +type AmlAddress struct { + Country string `json:"country"` + Postcode string `json:"post_code"` +} + +// AmlProfile User profile for Anti Money Laundering (AML) checks +type AmlProfile struct { + GivenNames string `json:"given_names"` + FamilyName string `json:"family_name"` + Address AmlAddress `json:"address"` + SSN string `json:"ssn"` +} + +// AmlResult Result of Anti Money Laundering (AML) check for a particular user +type AmlResult struct { + OnFraudList bool `json:"on_fraud_list"` + OnPEPList bool `json:"on_pep_list"` + OnWatchList bool `json:"on_watch_list"` +} + +// Deprecated: Will be removed in v3.0.0, please use GetAmlResult below instead. Parses AML result from response +func GetAmlResultFromResponse(amlResponse []byte) AmlResult { + var amlResult AmlResult + json.Unmarshal(amlResponse, &amlResult) + + return amlResult +} + +// GetAmlResult Parses AML result from response +func GetAmlResult(amlResponse []byte) (AmlResult, error) { + var amlResult AmlResult + err := json.Unmarshal(amlResponse, &amlResult) + + if err != nil { + log.Printf( + "Unable to get AML result from response. Error: %s", err) + return amlResult, err + } + + return amlResult, err +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/anchorparser.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/anchorparser.go new file mode 100644 index 0000000..efb5e16 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/anchorparser.go @@ -0,0 +1,114 @@ +package anchor + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + "log" + + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" + "github.com/getyoti/yoti-go-sdk/yotiprotocom" + "github.com/golang/protobuf/proto" +) + +type anchorExtension struct { + Extension string `asn1:"tag:0,utf8"` +} + +var ( + sourceOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 47127, 1, 1, 1} + verifierOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 47127, 1, 1, 2} +) + +// ParseAnchors takes a slice of protobuf anchors, parses them, and returns a slice of Yoti SDK Anchors +func ParseAnchors(protoAnchors []*yotiprotoattr.Anchor) []*Anchor { + var processedAnchors []*Anchor + for _, protoAnchor := range protoAnchors { + var extensions []string + var ( + anchorType = AnchorTypeUnknown + parsedCerts = parseCertificates(protoAnchor.OriginServerCerts) + ) + for _, cert := range parsedCerts { + for _, ext := range cert.Extensions { + var ( + value string + err error + ) + anchorType, value, err = parseExtension(ext) + if err != nil { + log.Printf("error parsing anchor extension, %v", err) + continue + } else if anchorType == AnchorTypeUnknown { + continue + } + extensions = append(extensions, value) + } + } + + processedAnchor := newAnchor(anchorType, parsedCerts, parseSignedTimestamp(protoAnchor.SignedTimeStamp), protoAnchor.SubType, extensions) + + processedAnchors = append(processedAnchors, processedAnchor) + } + + return processedAnchors +} + +func parseExtension(ext pkix.Extension) (anchorType Type, val string, err error) { + anchorType = AnchorTypeUnknown + + switch { + case ext.Id.Equal(sourceOID): + anchorType = AnchorTypeSource + case ext.Id.Equal(verifierOID): + anchorType = AnchorTypeVerifier + default: + return anchorType, "", nil + } + + var ae anchorExtension + _, err = asn1.Unmarshal(ext.Value, &ae) + switch { + case err != nil: + return anchorType, "", fmt.Errorf("unable to unmarshal extension: %v", err) + case len(ae.Extension) == 0: + return anchorType, "", errors.New("empty extension") + default: + val = ae.Extension + } + + return anchorType, val, nil +} + +func unmarshalExtension(extensionValue []byte) string { + var ae anchorExtension + + _, err := asn1.Unmarshal(extensionValue, &ae) + if err == nil && ae.Extension != "" { + return ae.Extension + } + + log.Printf("Error unmarshalling anchor extension: %q", err) + return "" +} + +func parseSignedTimestamp(rawBytes []byte) yotiprotocom.SignedTimestamp { + signedTimestamp := &yotiprotocom.SignedTimestamp{} + if err := proto.Unmarshal(rawBytes, signedTimestamp); err != nil { + signedTimestamp = nil + } + + return *signedTimestamp +} + +func parseCertificates(rawCerts [][]byte) (result []*x509.Certificate) { + for _, cert := range rawCerts { + parsedCertificate, _ := x509.ParseCertificate(cert) + + result = append(result, parsedCertificate) + } + + return result +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/anchors.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/anchors.go new file mode 100644 index 0000000..276b63f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/anchors.go @@ -0,0 +1,105 @@ +package anchor + +import ( + "crypto/x509" + + "github.com/getyoti/yoti-go-sdk/yotiprotocom" +) + +// Anchor is the metadata associated with an attribute. It describes how an attribute has been provided +// to Yoti (SOURCE Anchor) and how it has been verified (VERIFIER Anchor). +// If an attribute has only one SOURCE Anchor with the value set to +// "USER_PROVIDED" and zero VERIFIER Anchors, then the attribute +// is a self-certified one. +type Anchor struct { + anchorType Type + originServerCerts []*x509.Certificate + signedTimestamp SignedTimestamp + subtype string + value []string +} + +func newAnchor(anchorType Type, originServerCerts []*x509.Certificate, signedTimestamp yotiprotocom.SignedTimestamp, subtype string, value []string) *Anchor { + return &Anchor{ + anchorType: anchorType, + originServerCerts: originServerCerts, + signedTimestamp: convertSignedTimestamp(signedTimestamp), + subtype: subtype, + value: value, + } +} + +// Type Anchor type, based on the Object Identifier (OID) +type Type int + +const ( + // AnchorTypeUnknown - default value + AnchorTypeUnknown Type = 1 + iota + // AnchorTypeSource - how the anchor has been sourced + AnchorTypeSource + // AnchorTypeVerifier - how the anchor has been verified + AnchorTypeVerifier +) + +// Type of the Anchor - most likely either SOURCE or VERIFIER, but it's +// possible that new Anchor types will be added in future. +func (a Anchor) Type() Type { + return a.anchorType +} + +// OriginServerCerts are the X.509 certificate chain(DER-encoded ASN.1) +// from the service that assigned the attribute. +// +// The first certificate in the chain holds the public key that can be +// used to verify the Signature field; any following entries (zero or +// more) are for intermediate certificate authorities (in order). +// +// The last certificate in the chain must be verified against the Yoti root +// CA certificate. An extension in the first certificate holds the main artifact type, +// e.g. “PASSPORT”, which can be retrieved with .Value(). +func (a Anchor) OriginServerCerts() []*x509.Certificate { + return a.originServerCerts +} + +// SignedTimestamp is the time at which the signature was created. The +// message associated with the timestamp is the marshaled form of +// AttributeSigning (i.e. the same message that is signed in the +// Signature field). This method returns the SignedTimestamp +// object, the actual timestamp as a *time.Time can be called with +// .Timestamp() on the result of this function. +func (a Anchor) SignedTimestamp() SignedTimestamp { + return a.signedTimestamp +} + +// SubType is an indicator of any specific processing method, or +// subcategory, pertaining to an artifact. For example, for a passport, this would be +// either "NFC" or "OCR". +func (a Anchor) SubType() string { + return a.subtype +} + +// Value identifies the provider that either sourced or verified the attribute value. +// The range of possible values is not limited. For a SOURCE anchor, expect values like +// PASSPORT, DRIVING_LICENSE. For a VERIFIER anchor expect valuues like YOTI_ADMIN. +func (a Anchor) Value() []string { + return a.value +} + +// GetSources returns the anchors which identify how and when an attribute value was acquired. +func GetSources(anchors []*Anchor) (sources []*Anchor) { + return filterAnchors(anchors, AnchorTypeSource) +} + +// GetVerifiers returns the anchors which identify how and when an attribute value was verified by another provider. +func GetVerifiers(anchors []*Anchor) (sources []*Anchor) { + return filterAnchors(anchors, AnchorTypeVerifier) +} + +func filterAnchors(anchors []*Anchor, anchorType Type) (result []*Anchor) { + for _, v := range anchors { + if v.anchorType == anchorType { + result = append(result, v) + } + } + return result +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/signedtimestamp.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/signedtimestamp.go new file mode 100644 index 0000000..76f5529 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/anchor/signedtimestamp.go @@ -0,0 +1,35 @@ +package anchor + +import ( + "time" + + "github.com/getyoti/yoti-go-sdk/yotiprotocom" +) + +// SignedTimestamp is the object which contains a timestamp +type SignedTimestamp struct { + version int32 + timestamp *time.Time +} + +func convertSignedTimestamp(protoSignedTimestamp yotiprotocom.SignedTimestamp) SignedTimestamp { + uintTimestamp := protoSignedTimestamp.Timestamp + intTimestamp := int64(uintTimestamp) + unixTime := time.Unix(intTimestamp/1000000, 0) + + return SignedTimestamp{ + version: protoSignedTimestamp.Version, + timestamp: &unixTime, + } +} + +// Version indicates both the version of the protobuf message in use, +// as well as the specific hash algorithms. +func (s SignedTimestamp) Version() int32 { + return s.version +} + +// Timestamp is a point in time, to the nearest microsecond. +func (s SignedTimestamp) Timestamp() *time.Time { + return s.timestamp +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/genericattribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/genericattribute.go new file mode 100644 index 0000000..f0bc1ed --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/genericattribute.go @@ -0,0 +1,84 @@ +package attribute + +import ( + "log" + "time" + + "github.com/getyoti/yoti-go-sdk/anchor" + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" +) + +// GenericAttribute is a Yoti attribute which returns a generic value +type GenericAttribute struct { + *yotiprotoattr.Attribute + value interface{} + anchors []*anchor.Anchor +} + +// NewGeneric creates a new generic attribute +func NewGeneric(a *yotiprotoattr.Attribute) *GenericAttribute { + var value interface{} + + switch a.ContentType { + case yotiprotoattr.ContentType_DATE: + parsedTime, err := time.Parse("2006-01-02", string(a.Value)) + if err == nil { + value = &parsedTime + } else { + log.Printf("Unable to parse date value: %q. Error: %q", string(a.Value), err) + } + + case yotiprotoattr.ContentType_JSON: + unmarshalledJSON, err := UnmarshallJSON(a.Value) + + if err == nil { + value = unmarshalledJSON + } else { + log.Printf("Unable to parse JSON value: %q. Error: %q", string(a.Value), err) + } + + case yotiprotoattr.ContentType_STRING: + value = string(a.Value) + + case yotiprotoattr.ContentType_JPEG, + yotiprotoattr.ContentType_PNG, + yotiprotoattr.ContentType_UNDEFINED: + value = a.Value + + default: + value = a.Value + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &GenericAttribute{ + Attribute: &yotiprotoattr.Attribute{ + Name: a.Name, + ContentType: a.ContentType, + }, + value: value, + anchors: parsedAnchors, + } +} + +// Value returns the value of the GenericAttribute as an interface +func (a *GenericAttribute) Value() interface{} { + return a.value +} + +// Anchors are the metadata associated with an attribute. They describe +// how an attribute has been provided to Yoti (SOURCE Anchor) and how +// it has been verified (VERIFIER Anchor). +func (a *GenericAttribute) Anchors() []*anchor.Anchor { + return a.anchors +} + +// Sources returns the anchors which identify how and when an attribute value was acquired. +func (a *GenericAttribute) Sources() []*anchor.Anchor { + return anchor.GetSources(a.anchors) +} + +// Verifiers returns the anchors which identify how and when an attribute value was verified by another provider. +func (a *GenericAttribute) Verifiers() []*anchor.Anchor { + return anchor.GetVerifiers(a.anchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/image.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/image.go new file mode 100644 index 0000000..80fc242 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/image.go @@ -0,0 +1,33 @@ +package attribute + +import ( + "encoding/base64" + "fmt" +) + +const ( + // ImageTypeJpeg JPEG format + ImageTypeJpeg string = "jpeg" + // ImageTypePng PNG format + ImageTypePng string = "png" +) + +// Image format of the image and the image data +type Image struct { + Type string + Data []byte +} + +// GetMIMEType returns the MIME type of this piece of Yoti user information. For more information see: +// https://en.wikipedia.org/wiki/Media_type +func GetMIMEType(imageType string) string { + return fmt.Sprintf("image/%v", imageType) +} + +// Base64URL is the Image encoded as a base64 URL +func (image *Image) Base64URL() string { + base64EncodedImage := base64.StdEncoding.EncodeToString(image.Data) + contentType := GetMIMEType(image.Type) + + return "data:" + contentType + ";base64;," + base64EncodedImage +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/imageattribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/imageattribute.go new file mode 100644 index 0000000..9ddafbe --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/imageattribute.go @@ -0,0 +1,67 @@ +package attribute + +import ( + "errors" + + "github.com/getyoti/yoti-go-sdk/anchor" + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" +) + +// ImageAttribute is a Yoti attribute which returns an image as its value +type ImageAttribute struct { + *yotiprotoattr.Attribute + value *Image + anchors []*anchor.Anchor +} + +// NewImage creates a new Image attribute +func NewImage(a *yotiprotoattr.Attribute) (*ImageAttribute, error) { + var imageType string + + switch a.ContentType { + case yotiprotoattr.ContentType_JPEG: + imageType = ImageTypeJpeg + + case yotiprotoattr.ContentType_PNG: + imageType = ImageTypePng + + default: + return nil, errors.New("Cannot create ImageAttribute with unsupported type") + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &ImageAttribute{ + Attribute: &yotiprotoattr.Attribute{ + Name: a.Name, + ContentType: a.ContentType, + }, + value: &Image{ + Data: a.Value, + Type: imageType, + }, + anchors: parsedAnchors, + }, nil +} + +// Value returns the value of the ImageAttribute as *Image +func (a *ImageAttribute) Value() *Image { + return a.value +} + +// Anchors are the metadata associated with an attribute. They describe +// how an attribute has been provided to Yoti (SOURCE Anchor) and how +// it has been verified (VERIFIER Anchor). +func (a *ImageAttribute) Anchors() []*anchor.Anchor { + return a.anchors +} + +// Sources returns the anchors which identify how and when an attribute value was acquired. +func (a *ImageAttribute) Sources() []*anchor.Anchor { + return anchor.GetSources(a.anchors) +} + +// Verifiers returns the anchors which identify how and when an attribute value was verified by another provider. +func (a *ImageAttribute) Verifiers() []*anchor.Anchor { + return anchor.GetVerifiers(a.anchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/jsonattribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/jsonattribute.go new file mode 100644 index 0000000..e71ab69 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/jsonattribute.go @@ -0,0 +1,70 @@ +package attribute + +import ( + "encoding/json" + "fmt" + + "github.com/getyoti/yoti-go-sdk/anchor" + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" +) + +// JSONAttribute is a Yoti attribute which returns an interface as its value +type JSONAttribute struct { + *yotiprotoattr.Attribute // Value returns the value of a JSON attribute in the form of an interface + value interface{} + anchors []*anchor.Anchor +} + +// NewJSON creates a new JSON attribute +func NewJSON(a *yotiprotoattr.Attribute) (*JSONAttribute, error) { + interfaceValue, err := UnmarshallJSON(a.Value) + if err != nil { + err = fmt.Errorf("Unable to parse JSON value: %q. Error: %q", a.Value, err) + return nil, err + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &JSONAttribute{ + Attribute: &yotiprotoattr.Attribute{ + Name: a.Name, + ContentType: a.ContentType, + }, + value: interfaceValue, + anchors: parsedAnchors, + }, nil +} + +// UnmarshallJSON unmarshalls JSON into an interface +func UnmarshallJSON(byteValue []byte) (result interface{}, err error) { + var unmarshalledJSON interface{} + err = json.Unmarshal(byteValue, &unmarshalledJSON) + + if err != nil { + return nil, err + } + + return unmarshalledJSON, err +} + +// Value returns the value of the JSONAttribute as an interface +func (a *JSONAttribute) Value() interface{} { + return a.value +} + +// Anchors are the metadata associated with an attribute. They describe +// how an attribute has been provided to Yoti (SOURCE Anchor) and how +// it has been verified (VERIFIER Anchor). +func (a *JSONAttribute) Anchors() []*anchor.Anchor { + return a.anchors +} + +// Sources returns the anchors which identify how and when an attribute value was acquired. +func (a *JSONAttribute) Sources() []*anchor.Anchor { + return anchor.GetSources(a.anchors) +} + +// Verifiers returns the anchors which identify how and when an attribute value was verified by another provider. +func (a *JSONAttribute) Verifiers() []*anchor.Anchor { + return anchor.GetVerifiers(a.anchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/stringattribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/stringattribute.go new file mode 100644 index 0000000..018c8af --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/stringattribute.go @@ -0,0 +1,49 @@ +package attribute + +import ( + "github.com/getyoti/yoti-go-sdk/anchor" + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" +) + +// StringAttribute is a Yoti attribute which returns a string as its value +type StringAttribute struct { + *yotiprotoattr.Attribute + value string + anchors []*anchor.Anchor +} + +// NewString creates a new String attribute +func NewString(a *yotiprotoattr.Attribute) *StringAttribute { + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &StringAttribute{ + Attribute: &yotiprotoattr.Attribute{ + Name: a.Name, + ContentType: a.ContentType, + }, + value: string(a.Value), + anchors: parsedAnchors, + } +} + +// Value returns the value of the StringAttribute as a string +func (a *StringAttribute) Value() string { + return a.value +} + +// Anchors are the metadata associated with an attribute. They describe +// how an attribute has been provided to Yoti (SOURCE Anchor) and how +// it has been verified (VERIFIER Anchor). +func (a *StringAttribute) Anchors() []*anchor.Anchor { + return a.anchors +} + +// Sources returns the anchors which identify how and when an attribute value was acquired. +func (a *StringAttribute) Sources() []*anchor.Anchor { + return anchor.GetSources(a.anchors) +} + +// Verifiers returns the anchors which identify how and when an attribute value was verified by another provider. +func (a *StringAttribute) Verifiers() []*anchor.Anchor { + return anchor.GetVerifiers(a.anchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/timeattribute.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/timeattribute.go new file mode 100644 index 0000000..450a055 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/attribute/timeattribute.go @@ -0,0 +1,59 @@ +package attribute + +import ( + "log" + "time" + + "github.com/getyoti/yoti-go-sdk/anchor" + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" +) + +// TimeAttribute is a Yoti attribute which returns a time as its value +type TimeAttribute struct { + *yotiprotoattr.Attribute + value *time.Time + anchors []*anchor.Anchor +} + +// NewTime creates a new Time attribute +func NewTime(a *yotiprotoattr.Attribute) (*TimeAttribute, error) { + parsedTime, err := time.Parse("2006-01-02", string(a.Value)) + if err != nil { + log.Printf("Unable to parse time value of: %q. Error: %q", a.Value, err) + parsedTime = time.Time{} + return nil, err + } + + parsedAnchors := anchor.ParseAnchors(a.Anchors) + + return &TimeAttribute{ + Attribute: &yotiprotoattr.Attribute{ + Name: a.Name, + ContentType: a.ContentType, + }, + value: &parsedTime, + anchors: parsedAnchors, + }, nil +} + +// Value returns the value of the TimeAttribute as *time.Time +func (a *TimeAttribute) Value() *time.Time { + return a.value +} + +// Anchors are the metadata associated with an attribute. They describe +// how an attribute has been provided to Yoti (SOURCE Anchor) and how +// it has been verified (VERIFIER Anchor). +func (a *TimeAttribute) Anchors() []*anchor.Anchor { + return a.anchors +} + +// Sources returns the anchors which identify how and when an attribute value was acquired. +func (a *TimeAttribute) Sources() []*anchor.Anchor { + return anchor.GetSources(a.anchors) +} + +// Verifiers returns the anchors which identify how and when an attribute value was verified by another provider. +func (a *TimeAttribute) Verifiers() []*anchor.Anchor { + return anchor.GetVerifiers(a.anchors) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/conversion.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/conversion.go new file mode 100644 index 0000000..34739fe --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/conversion.go @@ -0,0 +1,28 @@ +package yoti + +import ( + "encoding/base64" +) + +func bytesToUtf8(bytes []byte) string { + return string(bytes) +} + +func bytesToBase64(bytes []byte) string { + return base64.StdEncoding.EncodeToString(bytes) +} + +func utfToBytes(utf8 string) []byte { + return []byte(utf8) +} + +func base64ToBytes(base64Str string) ([]byte, error) { + return base64.StdEncoding.DecodeString(base64Str) +} + +/* UrlSafe Base64 uses '-' and '_' instead of '+' and '/' respectively so it can be passed + * as a url parameter without extra encoding. + */ +func urlSafeBase64ToBytes(urlSafeBase64 string) ([]byte, error) { + return base64.URLEncoding.DecodeString(urlSafeBase64) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/crypto.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/crypto.go new file mode 100644 index 0000000..5689888 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/crypto.go @@ -0,0 +1,149 @@ +package yoti + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" +) + +func loadRsaKey(keyBytes []byte) (*rsa.PrivateKey, error) { + // Extract the PEM-encoded data + block, _ := pem.Decode(keyBytes) + + if block == nil { + return nil, errors.New("not PEM-encoded") + } + + if block.Type != "RSA PRIVATE KEY" { + return nil, errors.New("not RSA private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, errors.New("bad RSA private key") + } + + return key, nil +} + +func decryptRsa(cipherBytes []byte, key *rsa.PrivateKey) ([]byte, error) { + return rsa.DecryptPKCS1v15(rand.Reader, key, cipherBytes) +} + +func decipherAes(key, iv, cipherBytes []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return []byte{}, err + } + + // CBC mode always works in whole blocks. + if (len(cipherBytes) % aes.BlockSize) != 0 { + return []byte{}, errors.New("ciphertext is not a multiple of the block size") + } + + mode := cipher.NewCBCDecrypter(block, iv) + + decipheredBytes := make([]byte, len(cipherBytes)) + + mode.CryptBlocks(decipheredBytes, cipherBytes) + + return pkcs7Unpad(decipheredBytes, aes.BlockSize) +} + +func pkcs7Unpad(ciphertext []byte, blocksize int) (result []byte, err error) { + if blocksize <= 0 { + err = fmt.Errorf("blocksize %d is not valid for padding removal", blocksize) + return + } + if len(ciphertext) == 0 { + err = errors.New("Cannot remove padding on empty byte array") + return + } + if len(ciphertext)%blocksize != 0 { + err = errors.New("ciphertext is not a multiple of the block size") + return + } + + c := ciphertext[len(ciphertext)-1] + n := int(c) + if n == 0 || n > len(ciphertext) { + err = errors.New("ciphertext is not padded with PKCS#7 padding") + return + } + + // verify all padding bytes are correct + for i := 0; i < n; i++ { + if ciphertext[len(ciphertext)-n+i] != c { + err = errors.New("ciphertext is not padded with PKCS#7 padding") + return + } + } + return ciphertext[:len(ciphertext)-n], nil +} + +func signDigest(digest []byte, key *rsa.PrivateKey) ([]byte, error) { + hashed := sha256.Sum256(digest) + + signedDigest, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, hashed[:]) + if err != nil { + return []byte{}, err + } + + return signedDigest, nil +} + +func getAuthKey(key *rsa.PrivateKey) (string, error) { + return getDerEncodedPublicKey(key) +} + +func getDerEncodedPublicKey(key *rsa.PrivateKey) (result string, err error) { + var derEncodedBytes []byte + if derEncodedBytes, err = x509.MarshalPKIXPublicKey(key.Public()); err != nil { + return + } + + result = bytesToBase64(derEncodedBytes) + return +} + +func generateNonce() (string, error) { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + uuid := fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + + return uuid, nil +} + +func decryptToken(encryptedConnectToken string, key *rsa.PrivateKey) (result string, err error) { + // token was encoded as a urlsafe base64 so it can be transfered in a url + var cipherBytes []byte + if cipherBytes, err = urlSafeBase64ToBytes(encryptedConnectToken); err != nil { + return "", err + } + + var decipheredBytes []byte + if decipheredBytes, err = decryptRsa(cipherBytes, key); err != nil { + return "", err + } + + return bytesToUtf8(decipheredBytes), nil +} + +func unwrapKey(wrappedKey string, key *rsa.PrivateKey) (result []byte, err error) { + var cipherBytes []byte + if cipherBytes, err = base64ToBytes(wrappedKey); err != nil { + return nil, err + } + return decryptRsa(cipherBytes, key) +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/dataobjects.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/dataobjects.go new file mode 100644 index 0000000..f2b36be --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/dataobjects.go @@ -0,0 +1,20 @@ +package yoti + +type receiptDO struct { + ReceiptID string `json:"receipt_id"` + OtherPartyProfileContent string `json:"other_party_profile_content"` + ProfileContent string `json:"profile_content"` + OtherPartyExtraDataContent string `json:"other_party_extra_data_content"` + ExtraDataContent string `json:"extra_data_content"` + WrappedReceiptKey string `json:"wrapped_receipt_key"` + PolicyURI string `json:"policy_uri"` + PersonalKey string `json:"personal_key"` + RememberMeID string `json:"remember_me_id"` + SharingOutcome string `json:"sharing_outcome"` + Timestamp string `json:"timestamp"` +} + +type profileDO struct { + SessionData string `json:"session_data"` + Receipt receiptDO `json:"receipt"` +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/.env.example b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/.env.example new file mode 100644 index 0000000..36f05ae --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/.env.example @@ -0,0 +1,2 @@ +YOTI_CLIENT_SDK_ID= +YOTI_KEY_FILE_PATH= diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/.gitignore b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/.gitignore @@ -0,0 +1 @@ +.env diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/main.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/main.go new file mode 100644 index 0000000..6a7e30a --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/aml/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "io/ioutil" + "log" + "os" + "strconv" + + yoti "github.com/getyoti/yoti-go-sdk" + _ "github.com/joho/godotenv/autoload" +) + +var ( + sdkID string + key []byte + client *yoti.Client +) + +func main() { + var err error + key, err = ioutil.ReadFile(os.Getenv("YOTI_KEY_FILE_PATH")) + sdkID = os.Getenv("YOTI_CLIENT_SDK_ID") + + if err != nil { + log.Printf("Unable to retrieve `YOTI_KEY_FILE_PATH`. Error: `%s`", err) + return + } + + client = &yoti.Client{ + SdkID: sdkID, + Key: key} + + givenNames := "Edward Richard George" + familyName := "Heath" + + amlAddress := yoti.AmlAddress{ + Country: "GBR"} + + amlProfile := yoti.AmlProfile{ + GivenNames: givenNames, + FamilyName: familyName, + Address: amlAddress} + + result, err := client.PerformAmlCheck(amlProfile) + + if err != nil { + log.Printf( + "Unable to retrieve AML result. Error: %s", err) + } else { + log.Printf( + "AML Result for %s %s:", + givenNames, + familyName) + log.Printf( + "On PEP list: %s", + strconv.FormatBool(result.OnPEPList)) + log.Printf( + "On Fraud list: %s", + strconv.FormatBool(result.OnFraudList)) + log.Printf( + "On Watch list: %s", + strconv.FormatBool(result.OnWatchList)) + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/.env.example b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/.env.example new file mode 100644 index 0000000..68688ce --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/.env.example @@ -0,0 +1,4 @@ +YOTI_SCENARIO_ID= +YOTI_APPLICATION_ID= +YOTI_CLIENT_SDK_ID= +YOTI_KEY_FILE_PATH= diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/.gitignore b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/.gitignore @@ -0,0 +1 @@ +.env diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/certificatehelper.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/certificatehelper.go new file mode 100644 index 0000000..cdcb4f2 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/certificatehelper.go @@ -0,0 +1,175 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "net" + "os" + "strings" + "time" +) + +var ( + validFrom = "" + validFor = 2 * 365 * 24 * time.Hour + isCA = true + rsaBits = 2048 +) + +func publicKey(priv interface{}) interface{} { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + default: + return nil + } +} + +func pemBlockForKey(priv interface{}) *pem.Block { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} + case *ecdsa.PrivateKey: + b, err := x509.MarshalECPrivateKey(k) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to marshal ECDSA private key: %v", err) + os.Exit(2) + } + return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + default: + return nil + } +} + +func certificatePresenceCheck(certPath string, keyPath string) (present bool) { + if _, err := os.Stat(certPath); os.IsNotExist(err) { + return false + } + if _, err := os.Stat(keyPath); os.IsNotExist(err) { + return false + } + return true +} + +func generateSelfSignedCertificate(certPath, keyPath, host string) error { + priv, err := rsa.GenerateKey(rand.Reader, rsaBits) + if err != nil { + log.Printf("failed to generate private key: %s", err) + return err + } + + notBefore, err := parseNotBefore(validFrom) + if err != nil { + log.Printf("failed to parse 'Not Before' value of cert using validFrom %q, error was: %s", validFrom, err) + return err + } + + notAfter := notBefore.Add(validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Printf("failed to generate serial number: %s", err) + return err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Yoti"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + hosts := strings.Split(host, ",") + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + if isCA { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) + if err != nil { + log.Printf("Failed to create certificate: %s", err) + return err + } + + err = createPemFile(certPath, derBytes) + if err != nil { + log.Printf("failed to create pem file at %q: %s", certPath, err) + return err + } + log.Printf("written %s\n", certPath) + + err = createKeyFile(keyPath, priv) + if err != nil { + log.Printf("failed to create key file at %q: %s", keyPath, err) + return err + } + log.Printf("written %s\n", keyPath) + + return nil +} + +func createPemFile(certPath string, derBytes []byte) error { + certOut, err := os.Create(certPath) + + if err != nil { + log.Printf("failed to open "+certPath+" for writing: %s", err) + return err + } + + defer certOut.Close() + err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + + return err +} + +func createKeyFile(keyPath string, privateKey interface{}) error { + keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + + if err != nil { + log.Print("failed to open "+keyPath+" for writing:", err) + return err + } + + defer keyOut.Close() + err = pem.Encode(keyOut, pemBlockForKey(privateKey)) + + return err +} + +func parseNotBefore(validFrom string) (notBefore time.Time, err error) { + if len(validFrom) == 0 { + notBefore = time.Now() + } else { + notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse creation date: %s\n", err) + return time.Time{}, err + } + } + + return notBefore, nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/images/.keep b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/login.html b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/login.html new file mode 100644 index 0000000..ae22c31 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/login.html @@ -0,0 +1,15 @@ + + + + + Yoti Example Project + + + + Use Yoti + + + diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/main.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/main.go new file mode 100644 index 0000000..ac3e7a1 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/main.go @@ -0,0 +1,177 @@ +package main + +import ( + bytes "bytes" + "fmt" + "html/template" + "image" + "image/jpeg" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "strings" + + yoti "github.com/getyoti/yoti-go-sdk" + _ "github.com/joho/godotenv/autoload" +) + +var ( + sdkID string + key []byte + client *yoti.Client + selfSignedCertName = "yotiSelfSignedCert.pem" + selfSignedKeyName = "yotiSelfSignedKey.pem" + portNumber = "8080" +) + +func home(w http.ResponseWriter, req *http.Request) { + templateVars := map[string]interface{}{ + "yotiScenarioID": os.Getenv("YOTI_SCENARIO_ID"), + "yotiApplicationID": os.Getenv("YOTI_APPLICATION_ID")} + + t, err := template.ParseFiles("login.html") + + if err != nil { + panic("Error parsing the template: " + err.Error()) + } + + err = t.Execute(w, templateVars) + + if err != nil { + panic("Error applying the parsed template: " + err.Error()) + } +} + +func profile(w http.ResponseWriter, r *http.Request) { + var err error + key, err = ioutil.ReadFile(os.Getenv("YOTI_KEY_FILE_PATH")) + sdkID = os.Getenv("YOTI_CLIENT_SDK_ID") + + if err != nil { + log.Fatalf("Unable to retrieve `YOTI_KEY_FILE_PATH`. Error: `%s`", err) + } + + client = &yoti.Client{ + SdkID: sdkID, + Key: key} + + yotiOneTimeUseToken := r.URL.Query().Get("token") + + activityDetails, errStrings := client.GetActivityDetails(yotiOneTimeUseToken) + if len(errStrings) != 0 { + log.Fatalf("Errors: %v", errStrings) + } + + userProfile := activityDetails.UserProfile + + selfie := userProfile.Selfie() + var base64URL string + if selfie != nil { + base64URL = selfie.Value().Base64URL() + + decodedImage := decodeImage(selfie.Value().Data) + file := createImage() + saveImage(decodedImage, file) + } + + dob, err := userProfile.DateOfBirth() + if err != nil { + log.Fatalf("Error parsing Date of Birth attribute. Error %q", err) + } + + var dateOfBirthString string + if dob != nil { + dateOfBirthString = dob.Value().String() + } + + templateVars := map[string]interface{}{ + "profile": userProfile, + "selfieBase64URL": template.URL(base64URL), + "rememberMeID": activityDetails.RememberMeID(), + "dateOfBirth": dateOfBirthString, + } + + var t *template.Template + t, err = template.ParseFiles("profile.html") + if err != nil { + fmt.Println(err) + return + } + + err = t.Execute(w, templateVars) + + if err != nil { + panic("Error applying the parsed profile template. Error: " + err.Error()) + } +} + +func main() { + // Check if the cert files are available. + certificatePresent := certificatePresenceCheck(selfSignedCertName, selfSignedKeyName) + // If they are not available, generate new ones. + if !certificatePresent { + err := generateSelfSignedCertificate(selfSignedCertName, selfSignedKeyName, "127.0.0.1:"+portNumber) + if err != nil { + panic("Error when creating https certs: " + err.Error()) + } + } + + http.HandleFunc("/", home) + http.HandleFunc("/profile", profile) + + rootdir, err := os.Getwd() + if err != nil { + log.Fatal("Error: Couldn't get current working directory") + } + http.Handle("/images/", http.StripPrefix("/images", + http.FileServer(http.Dir(path.Join(rootdir, "images/"))))) + + log.Printf("About to listen and serve on %[1]s. Go to https://localhost:%[1]s/", portNumber) + err = http.ListenAndServeTLS(":"+portNumber, selfSignedCertName, selfSignedKeyName, nil) + + if err != nil { + panic("Error when calling `ListenAndServeTLS`: " + err.Error()) + } +} + +func redirectHandler(w http.ResponseWriter, req *http.Request) { + hostParts := strings.Split(req.Host, ":") + http.Redirect( + w, + req, + fmt.Sprintf("https://%s%s", hostParts[0], req.RequestURI), + http.StatusMovedPermanently) +} + +func decodeImage(imageBytes []byte) image.Image { + decodedImage, _, err := image.Decode(bytes.NewReader(imageBytes)) + + if err != nil { + panic("Error when decoding the image: " + err.Error()) + } + + return decodedImage +} + +func createImage() (file *os.File) { + file, err := os.Create("./images/YotiSelfie.jpeg") + + if err != nil { + panic("Error when creating the image: " + err.Error()) + } + return +} + +func saveImage(img image.Image, file io.Writer) { + var opt jpeg.Options + opt.Quality = 100 + + err := jpeg.Encode(file, img, &opt) + + if err != nil { + panic("Error when saving the image: " + err.Error()) + } +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/profile.html b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/profile.html new file mode 100644 index 0000000..234ae75 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/examples/profile/profile.html @@ -0,0 +1,95 @@ + + + + + Yoti Profile + + +

Home

+ + + + + + {{if .selfieBase64URL}} + + + + + + + + + {{end}} + {{if .profile.GivenNames}} + + + + + {{end}} + {{if .profile.FamilyName.Value}} + + + + + {{end}} + {{if .profile.FullName}} + + + + + {{end}} + {{if .profile.MobileNumber}} + + + + + {{end}} + {{if .profile.EmailAddress}} + + + + + {{end}} + {{if .dateOfBirth}} + + + + + {{end}} + {{if .profile.Address}} + + + + + {{end}} + {{if .profile.Gender}} + + + + + {{end}} + {{if .profile.Nationality}} + + + + + {{end}} + {{if .profile.StructuredPostalAddress}} + + + + + {{end}} +
Remember Me ID:{{.rememberMeID}}
Selfie as base64 URI:
Selfie from saved Image
First Name:{{.profile.GivenNames.Value}}
Family Name:{{.profile.FamilyName.Value}}
Full Name:{{.profile.FullName.Value}}
Mobile Number:{{.profile.MobileNumber.Value}}
Email Address:{{.profile.EmailAddress.Value}}
Date of Birth:{{.dateOfBirth}}
Address:{{.profile.Address.Value}}
Gender:{{.profile.Gender.Value}}
Nationality:{{.profile.Nationality.Value}}
Structured Postal Address: + + {{range $key, $value := .profile.StructuredPostalAddress.Value}} + + + + + {{end}} +
{{$key}}{{$value}}
+
+ + diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/httprequester.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/httprequester.go new file mode 100644 index 0000000..053b31c --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/httprequester.go @@ -0,0 +1,72 @@ +package yoti + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "net/http" +) + +const ( + // HTTPMethodPost Post HTTP method + HTTPMethodPost = "POST" + // HTTPMethodGet Get HTTP method + HTTPMethodGet = "GET" + // HTTPMethodPut Put HTTP method + HTTPMethodPut = "PUT" + // HTTPMethodPatch Patch HTTP method + HTTPMethodPatch = "PATCH" +) + +type httpResponse struct { + Success bool + StatusCode int + Content string +} + +type httpRequester func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) + +func doRequest(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + client := &http.Client{} + + supportedHTTPMethods := map[string]bool{"GET": true, "POST": true, "PUT": true, "PATCH": true} + + if !supportedHTTPMethods[httpRequestMethod] { + err = fmt.Errorf("HTTP Method: '%s' is unsupported", httpRequestMethod) + return + } + + var req *http.Request + if req, err = http.NewRequest( + httpRequestMethod, + uri, + bytes.NewBuffer(contentBytes)); err != nil { + return + } + + for key, value := range headers { + req.Header.Add(key, value) + } + + var resp *http.Response + resp, err = client.Do(req) + + if err != nil { + return + } + + defer resp.Body.Close() + + var responseBody []byte + if responseBody, err = ioutil.ReadAll(resp.Body); err != nil { + log.Printf("Unable to read the HTTP response, error: %s", err) + } + + result = &httpResponse{ + Success: resp.StatusCode < 300, + StatusCode: resp.StatusCode, + Content: string(responseBody)} + + return +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/image.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/image.go new file mode 100644 index 0000000..59badfa --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/image.go @@ -0,0 +1,45 @@ +package yoti + +import ( + "encoding/base64" +) + +// Deprecated: Will be removed in v3.0.0 - use attribute.ContentType instead. ImageType Image format +type ImageType int + +const ( + // ImageTypeJpeg JPEG format + ImageTypeJpeg ImageType = 1 + iota + // ImageTypePng PNG format + ImageTypePng + // ImageTypeOther Other image formats + ImageTypeOther +) + +// Deprecated: Will be removed in v3.0.0 - use attribute.Image instead. ImageType struct containing +// the type of the image and the data in bytes. +type Image struct { + Type ImageType + Data []byte +} + +// Deprecated: Will be removed in v3.0.0, please use image.GetMIMEType instead. GetContentType returns the MIME type of this piece of Yoti user information. For more information see: +// https://en.wikipedia.org/wiki/Media_type +func (image *Image) GetContentType() string { + switch image.Type { + case ImageTypeJpeg: + return "image/jpeg" + + case ImageTypePng: + return "image/png" + + default: + return "" + } +} + +// Deprecated: Will be removed in v3.0.0, please use image.Base64URL() instead. URL Image encoded in a base64 URL +func (image *Image) URL() string { + base64EncodedImage := base64.StdEncoding.EncodeToString(image.Data) + return "data:" + image.GetContentType() + ";base64;," + base64EncodedImage +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/login_flow.png b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/login_flow.png new file mode 100644 index 0000000..ca0e475 Binary files /dev/null and b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/login_flow.png differ diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/test-key-invalid-format.pem b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/test-key-invalid-format.pem new file mode 100644 index 0000000..dad3162 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/test-key-invalid-format.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAu7VR2P4kfOBMbsfFeaoTH7QNHVfS/VoallsiHLR9r2u52EfA +cnGELiCO8Z4I4OjMMg9yrP2Wcpyq4+pUdFW7GG2NkkPaTQpAlQ5Hm6xxgTGKahju +0OOGdLbWwol0S+QLFcnadj7/0pCSKe/v1XGR/iGZgyORHDzRMQHbLlBtMkC+wOIi +CUgnWkWhi0AJNoaEQoSvGdVAKjCOAimSbID9vGmnuK7FXCauMFRjbBh/Cbeyz4xl +w+BSvQmAGWSxpLyRinWXekNFtrm3YURwxKl4lpkEH6QQMgrImxMg+NURxNAsZob5 +QumxFopXO7ib0gNes47Ct5KyRPeF8CF+VLl6k9KWvQ3v7SYoypTXXwYfTbpqPhBr +mbPdqpIZ2oUKZJPRek68aU17YalAZ0jPNA30+UD2oKynYU2mif9UnrDxUnTnBnQX +F9tAsDWo/pOXwjKOYTKsxyBhbgh8rrgPFAqz00+qbk21gaiP4tjNMByBBMHzXUOg +GqlMjNNQhejTjL6rlXbJgmQDXPG4Xi+Q/+sUkrLNOTKY3FdnTw5PFUw9sRbP6+D6 +jS799P/OKay1DxuOPLw6r5QnfR2+pk9mNmgjVcBwwqt7gaUEjvDvj60ZppLZqQ8X +7Bv3zQetQHBtmhXyiuIH/UWXuj/VKLjnbaJtzZYTp8W4X8OT6vEwhLS9AncCAwEA +AQKCAgAIGBeBbeQQ5nMlS8P+LRFKCq+OFl1ow1vmI+PirP3GdLS82Ms5pB95Bbpk +PNZRLHixp+zf/MdiBdNwpIgjxBafRQoXxolBTTHfu4/m7JawZXx8errBky4XFlNI +bDjxlNHNjLi45JqPb+B9onULFSygcr514zC8sPqsTFIxOxKaWiRfmOCy2cOoptwC +by52hXJqk+IhEQsFRra47SX9O8q1NzEeS5sDED?uoZTv8lZ4Cs3RGVLCEYg/0osN +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 +-----END RSA PRIVATE KEY----- diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/test-key.pem b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/test-key.pem new file mode 100644 index 0000000..98c7641 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/test-key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchordrivinglicense.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchordrivinglicense.txt new file mode 100644 index 0000000..dfe875f --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchordrivinglicense.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchorpassport.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchorpassport.txt new file mode 100644 index 0000000..237f052 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchorpassport.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchoryotiadmin.txt b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchoryotiadmin.txt new file mode 100644 index 0000000..6333815 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/testanchoryotiadmin.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yoti_test.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yoti_test.go new file mode 100644 index 0000000..95e51bd --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yoti_test.go @@ -0,0 +1,1210 @@ +package yoti + +import ( + "encoding/base64" + "io/ioutil" + "log" + "math/big" + "net/url" + "reflect" + "strconv" + "strings" + "testing" + "time" + + "github.com/getyoti/yoti-go-sdk/anchor" + "github.com/getyoti/yoti-go-sdk/attribute" + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" + "github.com/golang/protobuf/proto" + "github.com/google/go-cmp/cmp" +) + +const ( + token = "NpdmVVGC-28356678-c236-4518-9de4-7a93009ccaf0-c5f92f2a-5539-453e-babc-9b06e1d6b7de" + encryptedToken = "b6H19bUCJhwh6WqQX_sEHWX9RP-A_ANr1fkApwA4Dp2nJQFAjrF9e6YCXhNBpAIhfHnN0iXubyXxXZMNwNMSQ5VOxkqiytrvPykfKQWHC6ypSbfy0ex8ihndaAXG5FUF-qcU8QaFPMy6iF3x0cxnY0Ij0kZj0Ng2t6oiNafb7AhT-VGXxbFbtZu1QF744PpWMuH0LVyBsAa5N5GJw2AyBrnOh67fWMFDKTJRziP5qCW2k4h5vJfiYr_EOiWKCB1d_zINmUm94ZffGXxcDAkq-KxhN1ZuNhGlJ2fKcFh7KxV0BqlUWPsIEiwS0r9CJ2o1VLbEs2U_hCEXaqseEV7L29EnNIinEPVbL4WR7vkF6zQCbK_cehlk2Qwda-VIATqupRO5grKZN78R9lBitvgilDaoE7JB_VFcPoljGQ48kX0wje1mviX4oJHhuO8GdFITS5LTbojGVQWT7LUNgAUe0W0j-FLHYYck3v84OhWTqads5_jmnnLkp9bdJSRuJF0e8pNdePnn2lgF-GIcyW_0kyGVqeXZrIoxnObLpF-YeUteRBKTkSGFcy7a_V_DLiJMPmH8UXDLOyv8TVt3ppzqpyUrLN2JVMbL5wZ4oriL2INEQKvw_boDJjZDGeRlu5m1y7vGDNBRDo64-uQM9fRUULPw-YkABNwC0DeShswzT00=" + sdkID = "fake-sdk-id" + wrappedReceiptKey = "kyHPjq2+Y48cx+9yS/XzmW09jVUylSdhbP+3Q9Tc9p6bCEnyfa8vj38AIu744RzzE+Dc4qkSF21VfzQKtJVILfOXu5xRc7MYa5k3zWhjiesg/gsrv7J4wDyyBpHIJB8TWXnubYMbSYQJjlsfwyxE9kGe0YI08pRo2Tiht0bfR5Z/YrhAk4UBvjp84D+oyug/1mtGhKphA4vgPhQ9/y2wcInYxju7Q6yzOsXGaRUXR38Tn2YmY9OBgjxiTnhoYJFP1X9YJkHeWMW0vxF1RHxgIVrpf7oRzdY1nq28qzRg5+wC7cjRpS2i/CKUAo0oVG4pbpXsaFhaTewStVC7UFtA77JHb3EnF4HcSWMnK5FM7GGkL9MMXQenh11NZHKPWXpux0nLZ6/vwffXZfsiyTIcFL/NajGN8C/hnNBljoQ+B3fzWbjcq5ueUOPwARZ1y38W83UwMynzkud/iEdHLaZIu4qUCRkfSxJg7Dc+O9/BdiffkOn2GyFmNjVeq754DCUypxzMkjYxokedN84nK13OU4afVyC7t5DDxAK/MqAc69NCBRLqMi5f8BMeOZfMcSWPGC9a2Qu8VgG125TuZT4+wIykUhGyj3Bb2/fdPsxwuKFR+E0uqs0ZKvcv1tkNRRtKYBqTacgGK9Yoehg12cyLrITLdjU1fmIDn4/vrhztN5w=" +) + +func TestYotiClient_KeyLoad_Failure(t *testing.T) { + key, _ := ioutil.ReadFile("test-key-invalid-format.pem") + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + result = &httpResponse{ + Success: false, + StatusCode: 500} + return + } + + _, _, errorStrings := getActivityDetails(requester, encryptedToken, sdkID, key) + + if len(errorStrings) == 0 { + t.Error("Expected failure") + } else if !strings.HasPrefix(errorStrings[0], "Invalid Key") { + t.Errorf("expected outcome type starting with %q instead received %q", "Invalid Key", errorStrings[0]) + } +} + +func TestYotiClient_HttpFailure_ReturnsFailure(t *testing.T) { + key, _ := ioutil.ReadFile("test-key.pem") + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + result = &httpResponse{ + Success: false, + StatusCode: 500} + return + } + + _, _, errorStrings := getActivityDetails(requester, encryptedToken, sdkID, key) + if len(errorStrings) == 0 { + t.Error("Expected failure") + } else if !strings.HasPrefix(errorStrings[0], ErrFailure.Error()) { + t.Errorf("expected outcome type %q instead received %q", ErrFailure.Error(), errorStrings[0]) + } +} + +func TestYotiClient_HttpFailure_ReturnsProfileNotFound(t *testing.T) { + key, _ := ioutil.ReadFile("test-key.pem") + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + result = &httpResponse{ + Success: false, + StatusCode: 404} + return + } + + _, _, errorStrings := getActivityDetails(requester, encryptedToken, sdkID, key) + if len(errorStrings) == 0 { + t.Error("Expected failure") + } else if !strings.HasPrefix(errorStrings[0], ErrProfileNotFound.Error()) { + t.Errorf("expected outcome type %q instead received %q", ErrProfileNotFound.Error(), errorStrings[0]) + } +} + +func TestYotiClient_SharingFailure_ReturnsFailure(t *testing.T) { + key, _ := ioutil.ReadFile("test-key.pem") + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + result = &httpResponse{ + Success: true, + StatusCode: 200, + Content: `{"session_data":"session_data","receipt":{"receipt_id": null,"other_party_profile_content": null,"policy_uri":null,"personal_key":null,"remember_me_id":null, "sharing_outcome":"FAILURE","timestamp":"2016-09-23T13:04:11Z"}}`} + return + } + + _, _, errorStrings := getActivityDetails(requester, encryptedToken, sdkID, key) + if len(errorStrings) == 0 { + t.Error("Expected failure") + } else if !strings.HasPrefix(errorStrings[0], ErrSharingFailure.Error()) { + t.Errorf("expected outcome type %q instead received %q", ErrSharingFailure.Error(), errorStrings[0]) + } +} + +func TestYotiClient_TokenDecodedSuccessfully(t *testing.T) { + key, _ := ioutil.ReadFile("test-key.pem") + + expectedAbsoluteURL := "/api/v1/profile/" + token + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + var theURL *url.URL + var theError error + if theURL, theError = url.Parse(uri); err != nil { + t.Errorf("Yoti api did not generate a valid uri. instead it generated: %s", theError) + } + + if theURL.Path != expectedAbsoluteURL { + t.Errorf("Yoti api did not generate a url path. expected %s, generated: %s", expectedAbsoluteURL, theURL.Path) + } + + result = &httpResponse{ + Success: false, + StatusCode: 500} + return + } + + _, _, errorStrings := getActivityDetails(requester, encryptedToken, sdkID, key) + if len(errorStrings) == 0 { + t.Error("Expected failure") + } else if !strings.HasPrefix(errorStrings[0], ErrFailure.Error()) { + t.Errorf("expected outcome type %q instead received %q", ErrFailure.Error(), errorStrings[0]) + } +} + +func TestYotiClient_ParseProfile_Success(t *testing.T) { + key, _ := ioutil.ReadFile("test-key.pem") + + otherPartyProfileContent := "ChCZAib1TBm9Q5GYfFrS1ep9EnAwQB5shpAPWLBgZgFgt6bCG3S5qmZHhrqUbQr3yL6yeLIDwbM7x4nuT/MYp+LDXgmFTLQNYbDTzrEzqNuO2ZPn9Kpg+xpbm9XtP7ZLw3Ep2BCmSqtnll/OdxAqLb4DTN4/wWdrjnFC+L/oQEECu646" + rememberMeID := "remember_me_id0123456789" + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + result = &httpResponse{ + Success: true, + StatusCode: 200, + Content: `{"receipt":{"wrapped_receipt_key": "` + wrappedReceiptKey + `","other_party_profile_content": "` + otherPartyProfileContent + `","remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS"}}`} + return + } + + userProfile, activityDetails, errorStrings := getActivityDetails(requester, encryptedToken, sdkID, key) + + if errorStrings != nil { + t.Error(errorStrings) + } + + if userProfile.ID != rememberMeID { + t.Errorf("expected id %q instead received %q", rememberMeID, userProfile.ID) + } + + if userProfile.Selfie == nil { + t.Error(`expected selfie attribute, but it was not present in the returned userProfile`) + } else if string(userProfile.Selfie.Data) != "selfie0123456789" { + t.Errorf("expected selfie attribute %q, instead received %q", "selfie0123456789", string(userProfile.Selfie.Data)) + } + + if userProfile.MobileNumber != "phone_number0123456789" { + t.Errorf("expected mobileNumber value %q, instead received %q", "phone_number0123456789", userProfile.MobileNumber) + } + + dobUserProfile := time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC) + if userProfile.DateOfBirth == nil { + t.Error(`expected date of birth but it was not present in the returned userProfile`) + } else if !userProfile.DateOfBirth.Equal(dobUserProfile) { + t.Errorf("expected date of birth %q, instead received %q", userProfile.DateOfBirth.Format(time.UnixDate), dobUserProfile.Format(time.UnixDate)) + } + + profile := activityDetails.UserProfile + + if activityDetails.RememberMeID() != rememberMeID { + t.Errorf("expected id %q, instead received %q", rememberMeID, activityDetails.RememberMeID()) + } + + expectedSelfieValue := "selfie0123456789" + if profile.Selfie() == nil { + t.Error(`expected selfie attribute, but it was not present in the returned profile`) + } else if !cmp.Equal(profile.Selfie().Value().Data, []byte(expectedSelfieValue)) { + t.Errorf("expected selfie %q, instead received %q", expectedSelfieValue, string(profile.Selfie().Value().Data)) + } + + if !cmp.Equal(profile.MobileNumber().Value(), "phone_number0123456789") { + t.Errorf("expected mobileNumber %q, instead received %q", "phone_number0123456789", profile.MobileNumber().Value()) + } + + expectedDoB := time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC) + + actualDoB, err := profile.DateOfBirth() + if err != nil { + t.Error(err) + } + + if actualDoB == nil { + t.Error(`expected date of birth, but it was not present in the returned profile`) + } else if !actualDoB.Value().Equal(expectedDoB) { + t.Errorf("expected date of birth: %q, instead received: %q", expectedDoB.Format(time.UnixDate), actualDoB.Value().Format(time.UnixDate)) + } +} + +func TestYotiClient_ParseWithoutProfile_Success(t *testing.T) { + key, _ := ioutil.ReadFile("test-key.pem") + rememberMeID := "remember_me_id0123456789" + + var otherPartyProfileContents = []string{ + `"other_party_profile_content": null,`, + `"other_party_profile_content": "",`, + ``} + + for _, otherPartyProfileContent := range otherPartyProfileContents { + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + result = &httpResponse{ + Success: true, + StatusCode: 200, + Content: `{"receipt":{"wrapped_receipt_key": "` + wrappedReceiptKey + `",` + + otherPartyProfileContent + `"remember_me_id":"` + rememberMeID + `", "sharing_outcome":"SUCCESS"}}`} + return + } + + userProfile, activityDetails, err := getActivityDetails(requester, encryptedToken, sdkID, key) + + if err != nil { + t.Error(err) + } + + if userProfile.ID != rememberMeID { + t.Errorf("expected id %q instead received %q", rememberMeID, userProfile.ID) + } + + if activityDetails.RememberMeID() != rememberMeID { + t.Errorf("expected id %q instead received %q", rememberMeID, activityDetails.RememberMeID()) + } + } +} + +func TestYotiClient_UnsupportedHttpMethod_ReturnsError(t *testing.T) { + uri := "http://www.url.com" + headers := CreateHeaders() + httpRequestMethod := "UNSUPPORTEDMETHOD" + contentBytes := make([]byte, 0) + + _, err := doRequest(uri, headers, httpRequestMethod, contentBytes) + + if err == nil { + t.Error("Expected failure") + } +} + +func TestYotiClient_SupportedHttpMethod(t *testing.T) { + uri := "http://www.url.com" + headers := CreateHeaders() + httpRequestMethod := HTTPMethodGet + contentBytes := make([]byte, 0) + + _, err := doRequest(uri, headers, httpRequestMethod, contentBytes) + + if err != nil { + t.Error(err) + } +} + +func TestYotiClient_PerformAmlCheck_Success(t *testing.T) { + key, _ := ioutil.ReadFile("test-key.pem") + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + + result = &httpResponse{ + Success: true, + StatusCode: 200, + Content: `{"on_fraud_list":true,"on_pep_list":true,"on_watch_list":true}`} + return + } + + result, err := performAmlCheck( + createStandardAmlProfile(), + requester, + sdkID, + key) + + if err != nil { + t.Error(err) + } + + if !result.OnFraudList { + t.Errorf("'OnFraudList' value is expected to be true") + } + if !result.OnPEPList { + t.Errorf("'OnPEPList' value is expected to be true") + } + if !result.OnWatchList { + t.Errorf("'OnWatchList' value is expected to be true") + } +} + +func TestYotiClient_PerformAmlCheck_Unsuccessful(t *testing.T) { + key, _ := ioutil.ReadFile("test-key.pem") + + var requester = func(uri string, headers map[string]string, httpRequestMethod string, contentBytes []byte) (result *httpResponse, err error) { + + result = &httpResponse{ + Success: false, + StatusCode: 503, + Content: `SERVICE UNAVAILABLE - Unable to reach the Integrity Service`} + return + } + + _, err := performAmlCheck( + createStandardAmlProfile(), + requester, + sdkID, + key) + + var expectedErrString = "AML Check was unsuccessful" + if err == nil { + t.Error("Expected failure") + } else if !strings.HasPrefix(err.Error(), expectedErrString) { + t.Errorf( + "expected outcome type starting with %q instead received %q", + expectedErrString, + err.Error()) + } +} + +func TestYotiClient_ParseIsAgeVerifiedValue_True(t *testing.T) { + trueValue := []byte("true") + + isAgeVerified, err := parseIsAgeVerifiedValue(trueValue) + + if err != nil { + t.Errorf("Failed to parse IsAgeVerified value, error was %q", err.Error()) + } + + if !*isAgeVerified { + t.Error("Expected true") + } +} + +func TestYotiClient_ParseIsAgeVerifiedValue_False(t *testing.T) { + falseValue := []byte("false") + + isAgeVerified, err := parseIsAgeVerifiedValue(falseValue) + + if err != nil { + t.Errorf("Failed to parse IsAgeVerified value, error was %q", err.Error()) + } + + if *isAgeVerified { + t.Error("Expected false") + } +} +func TestYotiClient_ParseIsAgeVerifiedValue_InvalidValueThrowsError(t *testing.T) { + invalidValue := []byte("invalidBool") + + _, err := parseIsAgeVerifiedValue(invalidValue) + + if err == nil { + t.Error("Expected error") + } +} +func TestYotiClient_UnmarshallJSONValue_InvalidValueThrowsError(t *testing.T) { + invalidStructuredAddress := []byte("invalidBool") + + _, err := attribute.UnmarshallJSON(invalidStructuredAddress) + + if err == nil { + t.Error("Expected error") + } +} + +func TestYotiClient_UnmarshallJSONValue_ValidValue(t *testing.T) { + const ( + countryIso = "IND" + nestedValue = "NestedValue" + ) + + var structuredAddress = []byte(`[ + { + "address_format": 2, + "building": "House No.86-A", + "state": "Punjab", + "postal_code": "141012", + "country_iso": "` + countryIso + `", + "country": "India", + "formatted_address": "House No.86-A\nRajgura Nagar\nLudhina\nPunjab\n141012\nIndia", + "1": + { + "1-1": + { + "1-1-1": "` + nestedValue + `" + } + } + } + ]`) + + parsedStructuredAddress, err := attribute.UnmarshallJSON(structuredAddress) + + if err != nil { + t.Errorf("Failed to parse structured address, error was %q", err.Error()) + } + + parsedStructuredAddressInterfaceSlice := parsedStructuredAddress.([]interface{}) + + parsedStructuredAddressMap := parsedStructuredAddressInterfaceSlice[0].(map[string]interface{}) + actualCountryIso := parsedStructuredAddressMap["country_iso"] + + if countryIso != actualCountryIso { + t.Errorf("expected country_iso: %q, actual value was: %q", countryIso, actualCountryIso) + } +} + +func TestYotiClient_MissingPostalAddress_UsesFormattedAddress(t *testing.T) { + var formattedAddressText = `House No.86-A\nRajgura Nagar\nLudhina\nPunjab\n141012\nIndia` + + var structuredAddressBytes = []byte(` + { + "address_format": 2, + "building": "House No.86-A", + "formatted_address": "` + formattedAddressText + `" + } + `) + + structuredAddress, err := attribute.UnmarshallJSON(structuredAddressBytes) + if err != nil { + t.Errorf("Failed to parse structured address, error was %q", err.Error()) + } + + var userProfile = UserProfile{ + ID: "remember_me_id0123456789", + OtherAttributes: make(map[string]AttributeValue), + StructuredPostalAddress: structuredAddress, + Address: ""} + + var jsonAttribute = &yotiprotoattr.Attribute{ + Name: attrConstStructuredPostalAddress, + Value: structuredAddressBytes, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: []*yotiprotoattr.Anchor{}, + } + + profile := createProfileWithSingleAttribute(jsonAttribute) + + profileAddress, profileErr := ensureAddressProfile(profile) + if profileErr != nil { + t.Errorf("Failed to add formatted address to address on Profile, error was %q", err.Error()) + } + + userProfileAddress, userProfileErr := ensureAddressUserProfile(userProfile) + if userProfileErr != nil { + t.Errorf("Failed to add formatted address to address on UserProfile, error was %q", err.Error()) + } + + escapedFormattedAddressText := strings.Replace(formattedAddressText, `\n`, "\n", -1) + + if profileAddress != escapedFormattedAddressText { + t.Errorf( + "Address does not equal the expected formatted address. address: %q, formatted address: %q", + profileAddress, + formattedAddressText) + } + + if userProfileAddress != escapedFormattedAddressText { + t.Errorf( + "Address does not equal the expected formatted address. address: %q, formatted address: %q", + userProfileAddress, + formattedAddressText) + } + + var structuredPostalAddress *attribute.JSONAttribute + + structuredPostalAddress, err = profile.StructuredPostalAddress() + if err != nil { + t.Error(err) + } + + if !cmp.Equal(structuredPostalAddress.ContentType, yotiprotoattr.ContentType_JSON) { + t.Errorf( + "Retrieved attribute does not have the correct type. Expected %q, actual: %q", + yotiprotoattr.ContentType_JSON, + structuredPostalAddress.ContentType) + } +} + +func TestYotiClient_PresentPostalAddress_DoesntUseFormattedAddress(t *testing.T) { + var addressText = `PostalAddress` + + var structuredAddressBytes = []byte(` + { + "address_format": 2, + "building": "House No.86-A", + "formatted_address": "FormattedAddress" + }`) + structuredAddress, err := attribute.UnmarshallJSON(structuredAddressBytes) + + if err != nil { + t.Errorf("Failed to parse structured address, error was %q", err.Error()) + } + + var result = UserProfile{ + ID: "remember_me_id0123456789", + OtherAttributes: make(map[string]AttributeValue), + StructuredPostalAddress: structuredAddress, + Address: addressText} + + newFormattedAddress, err := ensureAddressUserProfile(result) + + if err != nil { + t.Errorf("Failure when getting formatted address, error was %q", err.Error()) + } + + if newFormattedAddress != "" { + t.Errorf("Address should be unchanged when it is present, but it is : %q", newFormattedAddress) + } +} + +func TestYotiClient_MissingFormattedAddress_AddressUnchanged(t *testing.T) { + var structuredAddressBytes = []byte(` + { + "address_format": 2, + "building": "House No.86-A" + }`) + + structuredAddress, err := attribute.UnmarshallJSON(structuredAddressBytes) + + if err != nil { + t.Errorf("Failed to parse structured address, error was %q", err.Error()) + } + + var result = UserProfile{ + ID: "remember_me_id0123456789", + OtherAttributes: make(map[string]AttributeValue), + StructuredPostalAddress: structuredAddress, + Address: ""} + + address, err := ensureAddressUserProfile(result) + + if err != nil { + t.Errorf("Failed to add formatted address to address, error was %q", err.Error()) + } + + if address != "" { + t.Errorf("Formatted address missing, but address was still changed to: %q", address) + } +} + +func TestProfile_GetAttribute_String(t *testing.T) { + attributeName := "test_attribute_name" + attributeValueString := "value" + attributeValue := []byte(attributeValueString) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + if att.Name != attributeName { + t.Errorf( + "Retrieved attribute does not have the correct name. Expected %q, actual: %q", + attributeName, + att.Name) + } + + if !cmp.Equal(att.Value().(string), attributeValueString) { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %q, actual: %q", + attributeValue, + att.Value()) + } +} + +func TestProfile_GetAttribute_Time(t *testing.T) { + attributeName := "test_attribute_name" + + dateStringValue := "1985-01-01" + expectedDate := time.Date(1985, time.January, 1, 0, 0, 0, 0, time.UTC) + + attributeValueTime := []byte(dateStringValue) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValueTime, + ContentType: yotiprotoattr.ContentType_DATE, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + if !cmp.Equal(expectedDate, att.Value().(*time.Time).UTC()) { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %q, actual: %q", + expectedDate, + att.Value().(*time.Time)) + } +} + +func TestProfile_GetAttribute_Jpeg(t *testing.T) { + attributeName := "test_attribute_name" + attributeValue := []byte("value") + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + if !cmp.Equal(att.Value().([]byte), attributeValue) { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %q, actual: %q", + attributeValue, + att.Value()) + } +} + +func TestProfile_GetAttribute_Png(t *testing.T) { + attributeName := "test_attribute_name" + attributeValue := []byte("value") + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + if !cmp.Equal(att.Value().([]byte), attributeValue) { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %q, actual: %q", + attributeValue, + att.Value()) + } +} + +func TestProfile_GetAttribute_Bool(t *testing.T) { + attributeName := "test_attribute_name" + var initialBoolValue = true + attributeValue := []byte(strconv.FormatBool(initialBoolValue)) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + boolValue, err := strconv.ParseBool(att.Value().(string)) + if err != nil { + t.Errorf("Unable to parse string to bool. Error: %s", err) + } + + if !cmp.Equal(initialBoolValue, boolValue) { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %v, actual: %v", + initialBoolValue, + boolValue) + } +} + +func TestProfile_GetAttribute_JSON(t *testing.T) { + attributeName := "test_attribute_name" + addressFormat := "2" + + var structuredAddressBytes = []byte(` + { + "address_format": "` + addressFormat + `", + "building": "House No.86-A" + }`) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: structuredAddressBytes, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + retrievedAttributeMap := att.Value().(map[string]interface{}) + actualAddressFormat := retrievedAttributeMap["address_format"] + + if !cmp.Equal(actualAddressFormat, addressFormat) { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %q, actual: %q", + addressFormat, + actualAddressFormat) + } +} + +func TestProfile_GetAttribute_Undefined(t *testing.T) { + attributeName := "test_attribute_name" + attributeValueString := "value" + attributeValue := []byte(attributeValueString) + + var attr = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attr) + att := result.GetAttribute(attributeName) + + if att.Name != attributeName { + t.Errorf( + "Retrieved attribute does not have the correct name. Expected %q, actual: %q", + attributeName, + att.Name) + } + + if !cmp.Equal(att.Value().(string), attributeValueString) { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %q, actual: %q", + attributeValue, + att.Value()) + } +} +func TestProfile_GetAttribute_ReturnsNil(t *testing.T) { + result := Profile{ + attributeSlice: []*yotiprotoattr.Attribute{}, + } + + attribute := result.GetAttribute("attributeName") + + if attribute != nil { + t.Error("Attribute should not be retrieved if it is not present") + } +} + +func TestProfile_StringAttribute(t *testing.T) { + attributeName := attrConstNationality + attributeValueString := "value" + attributeValueBytes := []byte(attributeValueString) + + var as = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValueBytes, + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(as) + + if result.Nationality().Value() != attributeValueString { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %q, actual: %q", + attributeValueString, + result.Nationality().Value()) + } + + if !cmp.Equal(result.Nationality().ContentType, yotiprotoattr.ContentType_STRING) { + t.Errorf( + "Retrieved attribute does not have the correct type. Expected %q, actual: %q", + yotiprotoattr.ContentType_STRING, + result.Nationality().ContentType) + } +} + +func TestProfile_AttributeProperty_RetrievesAttribute(t *testing.T) { + attributeName := attrConstSelfie + attributeValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: attributeValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + if selfie.Name != attributeName { + t.Errorf( + "Retrieved attribute does not have the correct name. Expected %q, actual: %q", + attributeName, + selfie.Name) + } + + if !reflect.DeepEqual(attributeValue, selfie.Value().Data) { + t.Errorf( + "Retrieved attribute does not have the correct value. Expected %q, actual: %q", + attributeValue, + selfie.Value().Data) + } + + if !cmp.Equal(selfie.ContentType, yotiprotoattr.ContentType_PNG) { + t.Errorf( + "Retrieved attribute does not have the correct type. Expected %q, actual: %q", + yotiprotoattr.ContentType_PNG, + selfie.ContentType) + } +} + +func TestAttributeImage_Image_Png(t *testing.T) { + attributeName := attrConstSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + if !cmp.Equal(selfie.Value().Data, byteValue) { + t.Errorf( + "Retrieved attribute does not have the correct Image. Expected %v, actual: %v", + byteValue, + selfie.Value().Data) + } +} + +func TestAttributeImage_Image_Jpeg(t *testing.T) { + attributeName := attrConstSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + if !cmp.Equal(selfie.Value().Data, byteValue) { + t.Errorf( + "Retrieved attribute does not have the correct byte value. Expected %v, actual: %v", + byteValue, + selfie.Value().Data) + } +} + +func TestAttributeImage_Image_Default(t *testing.T) { + attributeName := attrConstSelfie + byteValue := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: byteValue, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + result := createProfileWithSingleAttribute(attributeImage) + selfie := result.Selfie() + + if !cmp.Equal(selfie.Value().Data, byteValue) { + t.Errorf( + "Retrieved attribute does not have the correct byte value. Expected %v, actual: %v", + byteValue, + selfie.Value().Data) + } +} +func TestAttributeImage_Base64Selfie_Png(t *testing.T) { + attributeName := attrConstSelfie + imageBytes := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: imageBytes, + ContentType: yotiprotoattr.ContentType_PNG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attributeImage) + + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(imageBytes) + + expectedBase64Selfie := "data:image/png;base64;," + base64ImageExpectedValue + + base64Selfie := result.Selfie().Value().Base64URL() + + if base64Selfie != expectedBase64Selfie { + t.Errorf( + "Base64Selfie does not have the correct value. Expected %q, actual: %q", + expectedBase64Selfie, + base64Selfie) + } +} + +func TestAttributeImage_Base64URL_Jpeg(t *testing.T) { + attributeName := attrConstSelfie + imageBytes := []byte("value") + + var attributeImage = &yotiprotoattr.Attribute{ + Name: attributeName, + Value: imageBytes, + ContentType: yotiprotoattr.ContentType_JPEG, + Anchors: []*yotiprotoattr.Anchor{}, + } + + result := createProfileWithSingleAttribute(attributeImage) + + base64ImageExpectedValue := base64.StdEncoding.EncodeToString(imageBytes) + + expectedBase64Selfie := "data:image/jpeg;base64;," + base64ImageExpectedValue + + base64Selfie := result.Selfie().Value().Base64URL() + + if base64Selfie != expectedBase64Selfie { + t.Errorf( + "Base64Selfie does not have the correct value. Expected %q, actual: %q", + expectedBase64Selfie, + base64Selfie) + } +} + +func TestAnchorParser_Passport(t *testing.T) { + log.SetOutput(ioutil.Discard) + + anchorSlice := CreateAnchorSliceFromTestFile(t, "testanchorpassport.txt") + + var structuredAddressBytes = []byte(` + { + "address_format": 2, + "building": "House No.86-A" + }`) + + a := &yotiprotoattr.Attribute{ + Name: attrConstStructuredPostalAddress, + Value: structuredAddressBytes, + ContentType: yotiprotoattr.ContentType_JSON, + Anchors: anchorSlice, + } + + result := createProfileWithSingleAttribute(a) + + var actualStructuredPostalAddress *attribute.JSONAttribute + + actualStructuredPostalAddress, err := result.StructuredPostalAddress() + + if err != nil { + t.Error(err) + } + + actualAnchor := actualStructuredPostalAddress.Anchors()[0] + + if actualAnchor != actualStructuredPostalAddress.Sources()[0] { + t.Error("Anchors and Sources should be the same when there is only one Source") + } + + if actualAnchor.Type() != anchor.AnchorTypeSource { + t.Errorf( + "Parsed anchor type is incorrect. Expected: %q, actual: %q", + anchor.AnchorTypeSource, + actualAnchor.Type()) + } + + expectedDate := time.Date(2018, time.April, 12, 13, 14, 32, 0, time.UTC) + actualDate := actualAnchor.SignedTimestamp().Timestamp().UTC() + if actualDate != expectedDate { + t.Errorf( + "Parsed anchor SignedTimestamp is incorrect. Expected: %q, actual: %q", + expectedDate, + actualDate) + } + + expectedSubType := "OCR" + if actualAnchor.SubType() != expectedSubType { + t.Errorf( + "Parsed anchor SubType is incorrect. Expected: %q, actual: %q", + expectedSubType, + actualAnchor.SubType()) + } + + expectedValue := "PASSPORT" + if actualAnchor.Value()[0] != expectedValue { + t.Errorf( + "Parsed anchor Value is incorrect. Expected: %q, actual: %q", + expectedValue, + actualAnchor.Value()[0]) + } + + actualSerialNo := actualAnchor.OriginServerCerts()[0].SerialNumber + AssertServerCertSerialNo(t, "277870515583559162487099305254898397834", actualSerialNo) +} + +func TestAnchorParser_DrivingLicense(t *testing.T) { + anchorSlice := CreateAnchorSliceFromTestFile(t, "testanchordrivinglicense.txt") + + attribute := &yotiprotoattr.Attribute{ + Name: attrConstGender, + Value: []byte("value"), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: anchorSlice, + } + + result := createProfileWithSingleAttribute(attribute) + + genderAttribute := result.Gender() + resultAnchor := genderAttribute.Anchors()[0] + + if resultAnchor != genderAttribute.Sources()[0] { + t.Error("Anchors and Sources should be the same when there is only one Source") + } + + if resultAnchor.Type() != anchor.AnchorTypeSource { + t.Errorf( + "Parsed anchor type is incorrect. Expected: %q, actual: %q", + anchor.AnchorTypeSource, + resultAnchor.Type()) + } + + expectedDate := time.Date(2018, time.April, 11, 12, 13, 3, 0, time.UTC) + actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC() + if actualDate != expectedDate { + t.Errorf( + "Parsed anchor SignedTimestamp is incorrect. Expected: %q, actual: %q", + expectedDate, + actualDate) + } + + expectedSubType := "" + if resultAnchor.SubType() != expectedSubType { + t.Errorf( + "Parsed anchor SubType is incorrect. Expected: %q, actual: %q", + expectedSubType, + resultAnchor.SubType()) + } + + expectedValue := "DRIVING_LICENCE" + if resultAnchor.Value()[0] != expectedValue { + t.Errorf( + "Parsed anchor Value is incorrect. Expected: %q, actual: %q", + expectedValue, + resultAnchor.Value()[0]) + } + + actualSerialNo := resultAnchor.OriginServerCerts()[0].SerialNumber + AssertServerCertSerialNo(t, "46131813624213904216516051554755262812", actualSerialNo) +} +func TestAnchorParser_YotiAdmin(t *testing.T) { + anchorSlice := CreateAnchorSliceFromTestFile(t, "testanchoryotiadmin.txt") + + attr := &yotiprotoattr.Attribute{ + Name: attrConstDateOfBirth, + Value: []byte("1999-01-01"), + ContentType: yotiprotoattr.ContentType_DATE, + Anchors: anchorSlice, + } + + result := createProfileWithSingleAttribute(attr) + + DoB, err := result.DateOfBirth() + + if err != nil { + t.Error(err) + } + + resultAnchor := DoB.Anchors()[0] + + if resultAnchor != DoB.Verifiers()[0] { + t.Error("Anchors and Verifiers should be the same when there is only one Verifier") + } + + if resultAnchor.Type() != anchor.AnchorTypeVerifier { + t.Errorf( + "Parsed anchor type is incorrect. Expected: %q, actual: %q", + anchor.AnchorTypeVerifier, + resultAnchor.Type()) + } + + expectedDate := time.Date(2018, time.April, 11, 12, 13, 4, 0, time.UTC) + actualDate := resultAnchor.SignedTimestamp().Timestamp().UTC() + if actualDate != expectedDate { + t.Errorf( + "Parsed anchor SignedTimestamp is incorrect. Expected: %q, actual: %q", + expectedDate, + actualDate) + } + + expectedSubType := "" + if resultAnchor.SubType() != expectedSubType { + t.Errorf( + "Parsed anchor SubType is incorrect. Expected: %q, actual: %q", + expectedSubType, + resultAnchor.SubType()) + } + + expectedValue := "YOTI_ADMIN" + if resultAnchor.Value()[0] != expectedValue { + t.Errorf( + "Parsed anchor Value is incorrect. Expected: %q, actual: %q", + expectedValue, + resultAnchor.Value()[0]) + } + + actualSerialNo := resultAnchor.OriginServerCerts()[0].SerialNumber + AssertServerCertSerialNo(t, "256616937783084706710155170893983549581", actualSerialNo) +} + +func TestAnchors_None(t *testing.T) { + anchorSlice := []*anchor.Anchor{} + + sources := anchor.GetSources(anchorSlice) + if len(sources) > 0 { + t.Error("GetSources should not return anything with empty anchors") + } + + verifiers := anchor.GetVerifiers(anchorSlice) + if len(verifiers) > 0 { + t.Error("GetVerifiers should not return anything with empty anchors") + } +} + +func createProfileWithSingleAttribute(attr *yotiprotoattr.Attribute) Profile { + var attributeSlice []*yotiprotoattr.Attribute + attributeSlice = append(attributeSlice, attr) + + return Profile{ + attributeSlice: attributeSlice, + } +} + +func AssertServerCertSerialNo(t *testing.T, expectedSerialNo string, actualSerialNo *big.Int) { + expectedSerialNoBigInt := new(big.Int) + expectedSerialNoBigInt, ok := expectedSerialNoBigInt.SetString(expectedSerialNo, 10) + if !ok { + t.Error("Unexpected error when setting string as big int") + } + + if expectedSerialNoBigInt.Cmp(actualSerialNo) != 0 { //0 == equivalent + t.Errorf( + "Parsed anchor OriginServerCerts is incorrect. Expected: %q, actual: %q", + expectedSerialNo, + actualSerialNo) + } +} + +func CreateAnchorSliceFromTestFile(t *testing.T, filename string) []*yotiprotoattr.Anchor { + anchorBytes, err := DecodeTestFile(t, filename) + + if err != nil { + t.Errorf("error decoding test file: %q", err) + } + + protoAnchor := &yotiprotoattr.Anchor{} + if err := proto.Unmarshal(anchorBytes, protoAnchor); err != nil { + t.Errorf("Error converting test anchor bytes into a Protobuf anchor. Error: %q", err) + } + + protoAnchors := append([]*yotiprotoattr.Anchor{}, protoAnchor) + + return protoAnchors +} + +func DecodeTestFile(t *testing.T, filename string) (result []byte, err error) { + base64Bytes := readTestFile(t, filename) + base64String := string(base64Bytes) + anchorBytes, err := base64.StdEncoding.DecodeString(base64String) + if err != nil { + return nil, err + } + return anchorBytes, nil +} + +func readTestFile(t *testing.T, filename string) (result []byte) { + b, err := ioutil.ReadFile(filename) + if err != nil { + t.Error(err) + } + + return b +} + +func CreateHeaders() (result map[string]string) { + + headers := make(map[string]string) + + headers["Header1"] = "test" + + return headers +} + +func createStandardAmlProfile() (result AmlProfile) { + var amlAddress = AmlAddress{ + Country: "GBR"} + + var amlProfile = AmlProfile{ + GivenNames: "Edward Richard George", + FamilyName: "Heath", + Address: amlAddress} + + return amlProfile +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiattributevalue.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiattributevalue.go new file mode 100644 index 0000000..9b754b5 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiattributevalue.go @@ -0,0 +1,60 @@ +package yoti + +import "log" + +// Deprecated: AttributeType format of the attribute +type AttributeType int + +const ( + // AttributeTypeDate date format + AttributeTypeDate AttributeType = 1 + iota + // AttributeTypeText text format + AttributeTypeText + // AttributeTypeJPEG JPEG format + AttributeTypeJPEG + // AttributeTypePNG PNG fornmat + AttributeTypePNG + // AttributeTypeJSON JSON fornmat + AttributeTypeJSON +) + +// Deprecated: Will be removed in v3.0.0, values here will be available on Attribute objects. AttributeValue represents a small piece of information about a Yoti user such as a photo of the user or the +// user's date of birth. +type AttributeValue struct { + // Type represents the format of the piece of user data, whether it is a date, a piece of text or a picture + // + // Note the potential values for this variable are stored in constants with names beginning with + // 'AttributeType'. These include: + // yoti.AttributeTypeDate + // yoti.AttributeTypeText + // yoti.AttributeTypeJPEG + // yoti.AttributeTypePNG + // yoti.AttributeTypeJSON + Type AttributeType + Value []byte +} + +// Deprecated: Will be removed in v3.0.0, use GetMIMEType() instead. GetContentType returns the MIME type of this piece of Yoti user information. For more information see: +// https://en.wikipedia.org/wiki/Media_type +func (val AttributeValue) GetContentType() (result string) { + switch val.Type { + case AttributeTypeDate: + result = "text/plain; charset=UTF-8" + + case AttributeTypeText: + result = "text/plain; charset=UTF-8" + + case AttributeTypeJPEG: + result = "image/jpeg" + + case AttributeTypePNG: + result = "image/png" + + case AttributeTypeJSON: + result = "application/json; charset=UTF-8" + + default: + log.Printf("Unable to find a matching MIME type for value type %q", val.Type) + } + return +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yoticlient.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yoticlient.go new file mode 100644 index 0000000..429eb33 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yoticlient.go @@ -0,0 +1,476 @@ +package yoti + +import ( + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "reflect" + "strconv" + "strings" + "time" + + "github.com/getyoti/yoti-go-sdk/attribute" + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" + "github.com/getyoti/yoti-go-sdk/yotiprotocom" + "github.com/golang/protobuf/proto" +) + +const ( + apiURL = "https://api.yoti.com/api/v1" + sdkIdentifier = "Go" + sdkVersionIdentifier = "2.3.1" + + authKeyHeader = "X-Yoti-Auth-Key" + authDigestHeader = "X-Yoti-Auth-Digest" + sdkIdentifierHeader = "X-Yoti-SDK" + sdkVersionIdentifierHeader = sdkIdentifierHeader + "-Version" + attributeAgeOver = "age_over:" + attributeAgeUnder = "age_under:" +) + +// Client represents a client that can communicate with yoti and return information about Yoti users. +type Client struct { + // SdkID represents the SDK ID and NOT the App ID. This can be found in the integration section of your + // application dashboard at https://www.yoti.com/dashboard/ + SdkID string + + // Key should be the security key given to you by yoti (see: security keys section of + // https://www.yoti.com/dashboard/) for more information about how to load your key from a file see: + // https://github.com/getyoti/yoti-go-sdk/blob/master/README.md + Key []byte +} + +// Deprecated: Will be removed in v3.0.0. Use `GetActivityDetails` instead. GetUserProfile requests information about a Yoti user using the one time use token generated by the Yoti login process. +// It returns the outcome of the request. If the request was successful it will include the users details, otherwise +// it will specify a reason the request failed. +func (client *Client) GetUserProfile(token string) (userProfile UserProfile, firstError error) { + var errStrings []string + userProfile, _, errStrings = getActivityDetails(doRequest, token, client.SdkID, client.Key) + if len(errStrings) > 0 { + firstError = errors.New(errStrings[0]) + } + return userProfile, firstError +} + +// GetActivityDetails requests information about a Yoti user using the one time use token generated by the Yoti login process. +// It returns the outcome of the request. If the request was successful it will include the users details, otherwise +// it will specify a reason the request failed. +func (client *Client) GetActivityDetails(token string) (ActivityDetails, []string) { + _, activityDetails, errStrings := getActivityDetails(doRequest, token, client.SdkID, client.Key) + return activityDetails, errStrings +} + +func getActivityDetails(requester httpRequester, encryptedToken, sdkID string, keyBytes []byte) (userProfile UserProfile, activityDetails ActivityDetails, errStrings []string) { + var err error + var key *rsa.PrivateKey + var httpMethod = HTTPMethodGet + + if key, err = loadRsaKey(keyBytes); err != nil { + errStrings = append(errStrings, fmt.Sprintf("Invalid Key: %s", err.Error())) + return + } + + // query parameters + var token string + if token, err = decryptToken(encryptedToken, key); err != nil { + errStrings = append(errStrings, err.Error()) + return + } + + var nonce string + if nonce, err = generateNonce(); err != nil { + errStrings = append(errStrings, err.Error()) + return + } + + timestamp := getTimestamp() + + // create http endpoint + endpoint := getProfileEndpoint(token, nonce, timestamp, sdkID) + + var headers map[string]string + if headers, err = createHeaders(key, httpMethod, endpoint, nil); err != nil { + errStrings = append(errStrings, err.Error()) + return + } + + var response *httpResponse + if response, err = requester(apiURL+endpoint, headers, httpMethod, nil); err != nil { + errStrings = append(errStrings, err.Error()) + return + } + + if response.Success { + var parsedResponse = profileDO{} + + if err = json.Unmarshal([]byte(response.Content), &parsedResponse); err != nil { + errStrings = append(errStrings, err.Error()) + return + } + + if parsedResponse.Receipt.SharingOutcome != "SUCCESS" { + err = ErrSharingFailure + errStrings = append(errStrings, err.Error()) + } else { + var attributeList *yotiprotoattr.AttributeList + if attributeList, err = decryptCurrentUserReceipt(&parsedResponse.Receipt, key); err != nil { + errStrings = append(errStrings, err.Error()) + return + } + id := parsedResponse.Receipt.RememberMeID + + userProfile = addAttributesToUserProfile(id, attributeList) //deprecated: will be removed in v3.0.0 + + profile := Profile{ + attributeSlice: createAttributeSlice(attributeList), + } + + var formattedAddress string + formattedAddress, err = ensureAddressProfile(profile) + if err != nil { + log.Printf("Unable to get 'Formatted Address' from 'Structured Postal Address'. Error: %q", err) + } else if formattedAddress != "" { + if _, err = profile.StructuredPostalAddress(); err != nil { + errStrings = append(errStrings, err.Error()) + return + } + + protoStructuredPostalAddress := getProtobufAttribute(profile, attrConstStructuredPostalAddress) + + addressAttribute := &yotiprotoattr.Attribute{ + Name: attrConstAddress, + Value: []byte(formattedAddress), + ContentType: yotiprotoattr.ContentType_STRING, + Anchors: protoStructuredPostalAddress.Anchors, + } + + profile.attributeSlice = append(profile.attributeSlice, addressAttribute) + } + + activityDetails = ActivityDetails{ + UserProfile: profile, + rememberMeID: id, + } + } + } else { + switch response.StatusCode { + case http.StatusNotFound: + err = ErrProfileNotFound + default: + err = ErrFailure + } + } + + if err != nil { + errStrings = append(errStrings, err.Error()) + } + + return userProfile, activityDetails, errStrings +} + +func getProtobufAttribute(profile Profile, key string) *yotiprotoattr.Attribute { + for _, v := range profile.attributeSlice { + if v.Name == attrConstStructuredPostalAddress { + return v + } + } + + return nil +} + +func addAttributesToUserProfile(id string, attributeList *yotiprotoattr.AttributeList) (result UserProfile) { + result = UserProfile{ + ID: id, + OtherAttributes: make(map[string]AttributeValue)} + + if attributeList == nil { + return + } + + for _, a := range attributeList.Attributes { + switch a.Name { + case "selfie": + + switch a.ContentType { + case yotiprotoattr.ContentType_JPEG: + result.Selfie = &Image{ + Type: ImageTypeJpeg, + Data: a.Value} + case yotiprotoattr.ContentType_PNG: + result.Selfie = &Image{ + Type: ImageTypePng, + Data: a.Value} + } + case "given_names": + result.GivenNames = string(a.Value) + case "family_name": + result.FamilyName = string(a.Value) + case "full_name": + result.FullName = string(a.Value) + case "phone_number": + result.MobileNumber = string(a.Value) + case "email_address": + result.EmailAddress = string(a.Value) + case "date_of_birth": + parsedTime, err := time.Parse("2006-01-02", string(a.Value)) + if err == nil { + result.DateOfBirth = &parsedTime + } else { + log.Printf("Unable to parse `date_of_birth` value: %q. Error: %q", a.Value, err) + } + case "postal_address": + result.Address = string(a.Value) + case "structured_postal_address": + structuredPostalAddress, err := attribute.UnmarshallJSON(a.Value) + + if err == nil { + result.StructuredPostalAddress = structuredPostalAddress + } else { + log.Printf("Unable to parse `structured_postal_address` value: %q. Error: %q", a.Value, err) + } + case "gender": + result.Gender = string(a.Value) + case "nationality": + result.Nationality = string(a.Value) + default: + if strings.HasPrefix(a.Name, attributeAgeOver) || + strings.HasPrefix(a.Name, attributeAgeUnder) { + + isAgeVerified, err := parseIsAgeVerifiedValue(a.Value) + + if err == nil { + result.IsAgeVerified = isAgeVerified + } else { + log.Printf("Unable to parse `IsAgeVerified` value: %q. Error: %q", a.Value, err) + } + } + + switch a.ContentType { + case yotiprotoattr.ContentType_DATE: + result.OtherAttributes[a.Name] = AttributeValue{ + Type: AttributeTypeDate, + Value: a.Value} + case yotiprotoattr.ContentType_STRING: + result.OtherAttributes[a.Name] = AttributeValue{ + Type: AttributeTypeText, + Value: a.Value} + case yotiprotoattr.ContentType_JPEG: + result.OtherAttributes[a.Name] = AttributeValue{ + Type: AttributeTypeJPEG, + Value: a.Value} + case yotiprotoattr.ContentType_PNG: + result.OtherAttributes[a.Name] = AttributeValue{ + Type: AttributeTypePNG, + Value: a.Value} + case yotiprotoattr.ContentType_JSON: + result.OtherAttributes[a.Name] = AttributeValue{ + Type: AttributeTypeJSON, + Value: a.Value} + } + } + } + formattedAddress, err := ensureAddressUserProfile(result) + if err != nil { + log.Printf("Unable to get 'Formatted Address' from 'Structured Postal Address'. Error: %q", err) + } else if formattedAddress != "" { + result.Address = formattedAddress + } + + return +} + +func createAttributeSlice(protoAttributeList *yotiprotoattr.AttributeList) (result []*yotiprotoattr.Attribute) { + if protoAttributeList != nil { + result = append(result, protoAttributeList.Attributes...) + } + + return result +} + +func ensureAddressUserProfile(result UserProfile) (address string, err error) { + if result.Address == "" && result.StructuredPostalAddress != nil { + var formattedAddress string + formattedAddress, err = retrieveFormattedAddressFromStructuredPostalAddress(result.StructuredPostalAddress) + if err == nil { + return formattedAddress, nil + } + } + + return "", err +} + +func ensureAddressProfile(profile Profile) (address string, err error) { + if profile.Address() == nil { + var structuredPostalAddress *attribute.JSONAttribute + if structuredPostalAddress, err = profile.StructuredPostalAddress(); err == nil { + if (structuredPostalAddress != nil && !reflect.DeepEqual(structuredPostalAddress, attribute.JSONAttribute{})) { + var formattedAddress string + formattedAddress, err = retrieveFormattedAddressFromStructuredPostalAddress(structuredPostalAddress.Value()) + if err == nil { + return formattedAddress, nil + } + } + } + } + + return "", err +} + +func retrieveFormattedAddressFromStructuredPostalAddress(structuredPostalAddress interface{}) (address string, err error) { + parsedStructuredAddressMap := structuredPostalAddress.(map[string]interface{}) + if formattedAddress, ok := parsedStructuredAddressMap["formatted_address"]; ok { + return formattedAddress.(string), nil + } + return +} + +func parseIsAgeVerifiedValue(byteValue []byte) (result *bool, err error) { + stringValue := string(byteValue) + + var parseResult bool + parseResult, err = strconv.ParseBool(stringValue) + + if err != nil { + return nil, err + } + + result = &parseResult + + return +} + +func decryptCurrentUserReceipt(receipt *receiptDO, key *rsa.PrivateKey) (result *yotiprotoattr.AttributeList, err error) { + var unwrappedKey []byte + if unwrappedKey, err = unwrapKey(receipt.WrappedReceiptKey, key); err != nil { + return + } + + if receipt.OtherPartyProfileContent == "" { + return + } + + var otherPartyProfileContentBytes []byte + if otherPartyProfileContentBytes, err = base64ToBytes(receipt.OtherPartyProfileContent); err != nil { + return + } + + encryptedData := &yotiprotocom.EncryptedData{} + if err = proto.Unmarshal(otherPartyProfileContentBytes, encryptedData); err != nil { + return nil, err + } + + var decipheredBytes []byte + if decipheredBytes, err = decipherAes(unwrappedKey, encryptedData.Iv, encryptedData.CipherText); err != nil { + return nil, err + } + + attributeList := &yotiprotoattr.AttributeList{} + if err := proto.Unmarshal(decipheredBytes, attributeList); err != nil { + return nil, err + } + + return attributeList, nil +} + +// PerformAmlCheck performs an Anti Money Laundering Check (AML) for a particular user. +// Returns three boolean values: 'OnPEPList', 'OnWatchList' and 'OnFraudList'. +func (client *Client) PerformAmlCheck(amlProfile AmlProfile) (AmlResult, error) { + return performAmlCheck(amlProfile, doRequest, client.SdkID, client.Key) +} + +func performAmlCheck(amlProfile AmlProfile, requester httpRequester, sdkID string, keyBytes []byte) (result AmlResult, err error) { + var key *rsa.PrivateKey + var httpMethod = HTTPMethodPost + + if key, err = loadRsaKey(keyBytes); err != nil { + err = fmt.Errorf("Invalid Key: %s", err.Error()) + return + } + + var nonce string + if nonce, err = generateNonce(); err != nil { + return + } + + timestamp := getTimestamp() + endpoint := getAMLEndpoint(nonce, timestamp, sdkID) + + var content []byte + if content, err = json.Marshal(amlProfile); err != nil { + return + } + + var headers map[string]string + if headers, err = createHeaders(key, httpMethod, endpoint, content); err != nil { + return + } + + var response *httpResponse + if response, err = requester(apiURL+endpoint, headers, httpMethod, content); err != nil { + return + } + + if response.Success { + result, err = GetAmlResult([]byte(response.Content)) + return + } + + err = fmt.Errorf( + "AML Check was unsuccessful, status code: '%d', content:'%s'", response.StatusCode, response.Content) + + return +} + +func getProfileEndpoint(token, nonce, timestamp, sdkID string) string { + return fmt.Sprintf("/profile/%s?nonce=%s×tamp=%s&appId=%s", token, nonce, timestamp, sdkID) +} + +func getAMLEndpoint(nonce, timestamp, sdkID string) string { + return fmt.Sprintf("/aml-check?appId=%s×tamp=%s&nonce=%s", sdkID, timestamp, nonce) +} + +func getAuthDigest(endpoint string, key *rsa.PrivateKey, httpMethod string, content []byte) (result string, err error) { + digest := httpMethod + "&" + endpoint + + if content != nil { + digest += "&" + bytesToBase64(content) + } + + digestBytes := utfToBytes(digest) + var signedDigestBytes []byte + + if signedDigestBytes, err = signDigest(digestBytes, key); err != nil { + return + } + + result = bytesToBase64(signedDigestBytes) + return +} + +func getTimestamp() string { + return strconv.FormatInt(time.Now().Unix()*1000, 10) +} + +func createHeaders(key *rsa.PrivateKey, httpMethod string, endpoint string, content []byte) (headers map[string]string, err error) { + var authKey string + if authKey, err = getAuthKey(key); err != nil { + return + } + + var authDigest string + if authDigest, err = getAuthDigest(endpoint, key, httpMethod, content); err != nil { + return + } + + headers = make(map[string]string) + + headers[authKeyHeader] = authKey + headers[authDigestHeader] = authDigest + headers[sdkIdentifierHeader] = sdkIdentifier + headers[sdkVersionIdentifierHeader] = sdkVersionIdentifier + + return headers, err +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprofile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprofile.go new file mode 100644 index 0000000..70f1e1d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprofile.go @@ -0,0 +1,151 @@ +package yoti + +import ( + "github.com/getyoti/yoti-go-sdk/attribute" + "github.com/getyoti/yoti-go-sdk/yotiprotoattr" +) + +const ( + attrConstSelfie = "selfie" + attrConstGivenNames = "given_names" + attrConstFamilyName = "family_name" + attrConstFullName = "full_name" + attrConstMobileNumber = "phone_number" + attrConstEmailAddress = "email_address" + attrConstDateOfBirth = "date_of_birth" + attrConstAddress = "postal_address" + attrConstStructuredPostalAddress = "structured_postal_address" + attrConstGender = "gender" + attrConstNationality = "nationality" +) + +// Profile represents the details retrieved for a particular user. Consists of +// Yoti attributes: a small piece of information about a Yoti user such as a +// photo of the user or the user's date of birth. +type Profile struct { + attributeSlice []*yotiprotoattr.Attribute +} + +// Selfie is a photograph of the user. Will be nil if not provided by Yoti +func (p Profile) Selfie() *attribute.ImageAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstSelfie { + attribute, err := attribute.NewImage(a) + + if err == nil { + return attribute + } + } + } + return nil +} + +// GivenNames represents the user's given names. Will be nil if not provided by Yoti +func (p Profile) GivenNames() *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstGivenNames { + return attribute.NewString(a) + } + } + return nil +} + +// FamilyName represents the user's family name. Will be nil if not provided by Yoti +func (p Profile) FamilyName() *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstFamilyName { + return attribute.NewString(a) + } + } + return nil +} + +// FullName represents the user's full name. Will be nil if not provided by Yoti +func (p Profile) FullName() *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstFullName { + return attribute.NewString(a) + } + } + return nil +} + +// MobileNumber represents the user's mobile phone number. Will be nil if not provided by Yoti +func (p Profile) MobileNumber() *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstMobileNumber { + return attribute.NewString(a) + } + } + return nil +} + +// EmailAddress represents the user's email address. Will be nil if not provided by Yoti +func (p Profile) EmailAddress() *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstEmailAddress { + return attribute.NewString(a) + } + } + return nil +} + +// DateOfBirth represents the user's date of birth. Will be nil if not provided by Yoti. Has an err value which will be filled if there is an error parsing the date. +func (p Profile) DateOfBirth() (*attribute.TimeAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == attrConstDateOfBirth { + return attribute.NewTime(a) + } + } + return nil, nil +} + +// Address represents the user's address. Will be nil if not provided by Yoti +func (p Profile) Address() *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstAddress { + return attribute.NewString(a) + } + } + return nil +} + +// StructuredPostalAddress represents the user's address in a JSON format. Will be nil if not provided by Yoti +func (p Profile) StructuredPostalAddress() (*attribute.JSONAttribute, error) { + for _, a := range p.attributeSlice { + if a.Name == attrConstStructuredPostalAddress { + return attribute.NewJSON(a) + } + } + return nil, nil +} + +// Gender represents the user's gender. Will be nil if not provided by Yoti +func (p Profile) Gender() *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstGender { + return attribute.NewString(a) + } + } + return nil +} + +// Nationality represents the user's nationality. Will be nil if not provided by Yoti +func (p Profile) Nationality() *attribute.StringAttribute { + for _, a := range p.attributeSlice { + if a.Name == attrConstNationality { + return attribute.NewString(a) + } + } + return nil +} + +// GetAttribute retrieve an attribute by name on the Yoti profile. Will return nil if attribute is not present. +func (p Profile) GetAttribute(attributeName string) *attribute.GenericAttribute { + for _, a := range p.attributeSlice { + if a.Name == attributeName { + return attribute.NewGeneric(a) + } + } + return nil +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Attribute.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Attribute.pb.go new file mode 100644 index 0000000..5287329 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Attribute.pb.go @@ -0,0 +1,245 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: Attribute.proto + +package yotiprotoattr + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// ContentType indicates how to interpret the ‘Value’ field of an Attribute. +type ContentType int32 + +const ( + // UNDEFINED should not be seen, and is used as an error placeholder + // value. + ContentType_UNDEFINED ContentType = 0 + // STRING means the value is UTF-8 encoded text. + ContentType_STRING ContentType = 1 + // JPEG indicates a standard .jpeg image. + ContentType_JPEG ContentType = 2 + // Date as string in RFC3339 format (YYYY-MM-DD). + ContentType_DATE ContentType = 3 + // PNG indicates a standard .png image. + ContentType_PNG ContentType = 4 + // JSON means the value is encoded using JSON. + ContentType_JSON ContentType = 5 +) + +var ContentType_name = map[int32]string{ + 0: "UNDEFINED", + 1: "STRING", + 2: "JPEG", + 3: "DATE", + 4: "PNG", + 5: "JSON", +} + +var ContentType_value = map[string]int32{ + "UNDEFINED": 0, + "STRING": 1, + "JPEG": 2, + "DATE": 3, + "PNG": 4, + "JSON": 5, +} + +func (x ContentType) String() string { + return proto.EnumName(ContentType_name, int32(x)) +} + +func (ContentType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_44d6ebe2a990cc1d, []int{0} +} + +type Attribute struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + ContentType ContentType `protobuf:"varint,3,opt,name=content_type,json=contentType,proto3,enum=attrpubapi_v1.ContentType" json:"content_type,omitempty"` + Anchors []*Anchor `protobuf:"bytes,4,rep,name=anchors,proto3" json:"anchors,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Attribute) Reset() { *m = Attribute{} } +func (m *Attribute) String() string { return proto.CompactTextString(m) } +func (*Attribute) ProtoMessage() {} +func (*Attribute) Descriptor() ([]byte, []int) { + return fileDescriptor_44d6ebe2a990cc1d, []int{0} +} + +func (m *Attribute) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Attribute.Unmarshal(m, b) +} +func (m *Attribute) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Attribute.Marshal(b, m, deterministic) +} +func (m *Attribute) XXX_Merge(src proto.Message) { + xxx_messageInfo_Attribute.Merge(m, src) +} +func (m *Attribute) XXX_Size() int { + return xxx_messageInfo_Attribute.Size(m) +} +func (m *Attribute) XXX_DiscardUnknown() { + xxx_messageInfo_Attribute.DiscardUnknown(m) +} + +var xxx_messageInfo_Attribute proto.InternalMessageInfo + +func (m *Attribute) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Attribute) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func (m *Attribute) GetContentType() ContentType { + if m != nil { + return m.ContentType + } + return ContentType_UNDEFINED +} + +func (m *Attribute) GetAnchors() []*Anchor { + if m != nil { + return m.Anchors + } + return nil +} + +type Anchor struct { + ArtifactLink []byte `protobuf:"bytes,1,opt,name=artifact_link,json=artifactLink,proto3" json:"artifact_link,omitempty"` + OriginServerCerts [][]byte `protobuf:"bytes,2,rep,name=origin_server_certs,json=originServerCerts,proto3" json:"origin_server_certs,omitempty"` + ArtifactSignature []byte `protobuf:"bytes,3,opt,name=artifact_signature,json=artifactSignature,proto3" json:"artifact_signature,omitempty"` + SubType string `protobuf:"bytes,4,opt,name=sub_type,json=subType,proto3" json:"sub_type,omitempty"` + Signature []byte `protobuf:"bytes,5,opt,name=signature,proto3" json:"signature,omitempty"` + SignedTimeStamp []byte `protobuf:"bytes,6,opt,name=signed_time_stamp,json=signedTimeStamp,proto3" json:"signed_time_stamp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Anchor) Reset() { *m = Anchor{} } +func (m *Anchor) String() string { return proto.CompactTextString(m) } +func (*Anchor) ProtoMessage() {} +func (*Anchor) Descriptor() ([]byte, []int) { + return fileDescriptor_44d6ebe2a990cc1d, []int{1} +} + +func (m *Anchor) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Anchor.Unmarshal(m, b) +} +func (m *Anchor) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Anchor.Marshal(b, m, deterministic) +} +func (m *Anchor) XXX_Merge(src proto.Message) { + xxx_messageInfo_Anchor.Merge(m, src) +} +func (m *Anchor) XXX_Size() int { + return xxx_messageInfo_Anchor.Size(m) +} +func (m *Anchor) XXX_DiscardUnknown() { + xxx_messageInfo_Anchor.DiscardUnknown(m) +} + +var xxx_messageInfo_Anchor proto.InternalMessageInfo + +func (m *Anchor) GetArtifactLink() []byte { + if m != nil { + return m.ArtifactLink + } + return nil +} + +func (m *Anchor) GetOriginServerCerts() [][]byte { + if m != nil { + return m.OriginServerCerts + } + return nil +} + +func (m *Anchor) GetArtifactSignature() []byte { + if m != nil { + return m.ArtifactSignature + } + return nil +} + +func (m *Anchor) GetSubType() string { + if m != nil { + return m.SubType + } + return "" +} + +func (m *Anchor) GetSignature() []byte { + if m != nil { + return m.Signature + } + return nil +} + +func (m *Anchor) GetSignedTimeStamp() []byte { + if m != nil { + return m.SignedTimeStamp + } + return nil +} + +func init() { + proto.RegisterEnum("attrpubapi_v1.ContentType", ContentType_name, ContentType_value) + proto.RegisterType((*Attribute)(nil), "attrpubapi_v1.Attribute") + proto.RegisterType((*Anchor)(nil), "attrpubapi_v1.Anchor") +} + +func init() { proto.RegisterFile("Attribute.proto", fileDescriptor_44d6ebe2a990cc1d) } + +var fileDescriptor_44d6ebe2a990cc1d = []byte{ + // 400 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x92, 0xcf, 0x6e, 0xd3, 0x40, + 0x10, 0xc6, 0x71, 0xec, 0x24, 0xcd, 0xc4, 0xa1, 0xce, 0xf0, 0x47, 0x06, 0x71, 0xb0, 0xca, 0xc5, + 0xaa, 0x84, 0x11, 0xe9, 0x99, 0x43, 0xda, 0x84, 0xa8, 0x08, 0xb9, 0xd1, 0x3a, 0x5c, 0xb8, 0x58, + 0x6b, 0xb3, 0x94, 0x55, 0xeb, 0xb5, 0xb5, 0x1e, 0x47, 0xca, 0x03, 0xf1, 0x80, 0xbc, 0x01, 0xf2, + 0x5a, 0x6e, 0x68, 0x6f, 0x33, 0xdf, 0xef, 0xdb, 0x4f, 0x3b, 0xa3, 0x81, 0xd3, 0x25, 0x91, 0x96, + 0x59, 0x43, 0x22, 0xaa, 0x74, 0x49, 0x25, 0xce, 0x38, 0x91, 0xae, 0x9a, 0x8c, 0x57, 0x32, 0xdd, + 0x7f, 0x3a, 0xfb, 0x63, 0xc1, 0xe4, 0xc1, 0x82, 0x08, 0x8e, 0xe2, 0x85, 0xf0, 0xad, 0xc0, 0x0a, + 0x27, 0xcc, 0xd4, 0xf8, 0x12, 0x86, 0x7b, 0x7e, 0xdf, 0x08, 0x7f, 0x10, 0x58, 0xa1, 0xcb, 0xba, + 0x06, 0x3f, 0x83, 0x9b, 0x97, 0x8a, 0x84, 0xa2, 0x94, 0x0e, 0x95, 0xf0, 0xed, 0xc0, 0x0a, 0x9f, + 0x2f, 0xde, 0x46, 0x8f, 0xd2, 0xa3, 0xab, 0xce, 0xb2, 0x3b, 0x54, 0x82, 0x4d, 0xf3, 0x63, 0x83, + 0x1f, 0x61, 0xcc, 0x55, 0xfe, 0xbb, 0xd4, 0xb5, 0xef, 0x04, 0x76, 0x38, 0x5d, 0xbc, 0x7a, 0xf2, + 0x72, 0x69, 0x28, 0xeb, 0x5d, 0x67, 0x7f, 0x2d, 0x18, 0x75, 0x1a, 0xbe, 0x87, 0x19, 0xd7, 0x24, + 0x7f, 0xf1, 0x9c, 0xd2, 0x7b, 0xa9, 0xee, 0xcc, 0x6f, 0x5d, 0xe6, 0xf6, 0xe2, 0x37, 0xa9, 0xee, + 0x30, 0x82, 0x17, 0xa5, 0x96, 0xb7, 0x52, 0xa5, 0xb5, 0xd0, 0x7b, 0xa1, 0xd3, 0x5c, 0x68, 0xaa, + 0xfd, 0x41, 0x60, 0x87, 0x2e, 0x9b, 0x77, 0x28, 0x31, 0xe4, 0xaa, 0x05, 0xf8, 0x01, 0xf0, 0x21, + 0xb4, 0x96, 0xb7, 0x8a, 0x53, 0xa3, 0xbb, 0xa9, 0x5c, 0x36, 0xef, 0x49, 0xd2, 0x03, 0x7c, 0x03, + 0x27, 0x75, 0x93, 0x75, 0xa3, 0x3b, 0x66, 0x59, 0xe3, 0xba, 0xc9, 0xcc, 0x68, 0xef, 0x60, 0x72, + 0x0c, 0x18, 0x9a, 0x80, 0xa3, 0x80, 0xe7, 0x30, 0x6f, 0x1b, 0xf1, 0x33, 0x25, 0x59, 0x88, 0xb4, + 0x26, 0x5e, 0x54, 0xfe, 0xc8, 0xb8, 0x4e, 0x3b, 0xb0, 0x93, 0x85, 0x48, 0x5a, 0xf9, 0xfc, 0x06, + 0xa6, 0xff, 0x2d, 0x10, 0x67, 0x30, 0xf9, 0x1e, 0xaf, 0xd6, 0x5f, 0xae, 0xe3, 0xf5, 0xca, 0x7b, + 0x86, 0x00, 0xa3, 0x64, 0xc7, 0xae, 0xe3, 0x8d, 0x67, 0xe1, 0x09, 0x38, 0x5f, 0xb7, 0xeb, 0x8d, + 0x37, 0x68, 0xab, 0xd5, 0x72, 0xb7, 0xf6, 0x6c, 0x1c, 0x83, 0xbd, 0x8d, 0x37, 0x9e, 0x63, 0x60, + 0x72, 0x13, 0x7b, 0xc3, 0xcb, 0x05, 0xbc, 0xce, 0xcb, 0x22, 0x3a, 0x94, 0x24, 0x1f, 0xad, 0xfb, + 0xe2, 0xd2, 0xdc, 0xc0, 0xb6, 0x3d, 0x90, 0x1f, 0xb3, 0x16, 0x9b, 0x5b, 0x69, 0x2d, 0xd9, 0xc8, + 0x94, 0x17, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0xbf, 0xae, 0x17, 0x38, 0x49, 0x02, 0x00, 0x00, +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Attribute.proto b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Attribute.proto new file mode 100644 index 0000000..94179ff --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Attribute.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package attrpubapi_v1; + +option java_package = "com.yoti.api.client.spi.remote.proto"; +option java_outer_classname = "AttrProto"; + +option go_package = "yotiprotoattr"; + + +// ContentType indicates how to interpret the ‘Value’ field of an Attribute. +enum ContentType { + // UNDEFINED should not be seen, and is used as an error placeholder + // value. + UNDEFINED = 0; + + // STRING means the value is UTF-8 encoded text. + STRING = 1; + + // JPEG indicates a standard .jpeg image. + JPEG = 2; + + // Date as string in RFC3339 format (YYYY-MM-DD). + DATE = 3; + + // PNG indicates a standard .png image. + PNG = 4; + + // JSON means the value is encoded using JSON. + JSON = 5; +} + + +message Attribute { + string name = 1; + + bytes value = 2; + + ContentType content_type = 3; + + repeated Anchor anchors = 4; +} + + +message Anchor { + bytes artifact_link = 1; + + repeated bytes origin_server_certs = 2; + + bytes artifact_signature = 3; + + string sub_type = 4; + + bytes signature = 5; + + bytes signed_time_stamp = 6; +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/List.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/List.pb.go new file mode 100644 index 0000000..e75b566 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/List.pb.go @@ -0,0 +1,174 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: List.proto + +package yotiprotoattr + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// AttributeAndId is a simple container for holding an attribute's value +// alongside its ID. +type AttributeAndId struct { + Attribute *Attribute `protobuf:"bytes,1,opt,name=attribute,proto3" json:"attribute,omitempty"` + AttributeId []byte `protobuf:"bytes,2,opt,name=attribute_id,json=attributeId,proto3" json:"attribute_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AttributeAndId) Reset() { *m = AttributeAndId{} } +func (m *AttributeAndId) String() string { return proto.CompactTextString(m) } +func (*AttributeAndId) ProtoMessage() {} +func (*AttributeAndId) Descriptor() ([]byte, []int) { + return fileDescriptor_e23ec92774814a82, []int{0} +} + +func (m *AttributeAndId) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AttributeAndId.Unmarshal(m, b) +} +func (m *AttributeAndId) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AttributeAndId.Marshal(b, m, deterministic) +} +func (m *AttributeAndId) XXX_Merge(src proto.Message) { + xxx_messageInfo_AttributeAndId.Merge(m, src) +} +func (m *AttributeAndId) XXX_Size() int { + return xxx_messageInfo_AttributeAndId.Size(m) +} +func (m *AttributeAndId) XXX_DiscardUnknown() { + xxx_messageInfo_AttributeAndId.DiscardUnknown(m) +} + +var xxx_messageInfo_AttributeAndId proto.InternalMessageInfo + +func (m *AttributeAndId) GetAttribute() *Attribute { + if m != nil { + return m.Attribute + } + return nil +} + +func (m *AttributeAndId) GetAttributeId() []byte { + if m != nil { + return m.AttributeId + } + return nil +} + +type AttributeAndIdList struct { + AttributeAndIdList []*AttributeAndId `protobuf:"bytes,1,rep,name=attribute_and_id_list,json=attributeAndIdList,proto3" json:"attribute_and_id_list,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AttributeAndIdList) Reset() { *m = AttributeAndIdList{} } +func (m *AttributeAndIdList) String() string { return proto.CompactTextString(m) } +func (*AttributeAndIdList) ProtoMessage() {} +func (*AttributeAndIdList) Descriptor() ([]byte, []int) { + return fileDescriptor_e23ec92774814a82, []int{1} +} + +func (m *AttributeAndIdList) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AttributeAndIdList.Unmarshal(m, b) +} +func (m *AttributeAndIdList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AttributeAndIdList.Marshal(b, m, deterministic) +} +func (m *AttributeAndIdList) XXX_Merge(src proto.Message) { + xxx_messageInfo_AttributeAndIdList.Merge(m, src) +} +func (m *AttributeAndIdList) XXX_Size() int { + return xxx_messageInfo_AttributeAndIdList.Size(m) +} +func (m *AttributeAndIdList) XXX_DiscardUnknown() { + xxx_messageInfo_AttributeAndIdList.DiscardUnknown(m) +} + +var xxx_messageInfo_AttributeAndIdList proto.InternalMessageInfo + +func (m *AttributeAndIdList) GetAttributeAndIdList() []*AttributeAndId { + if m != nil { + return m.AttributeAndIdList + } + return nil +} + +type AttributeList struct { + Attributes []*Attribute `protobuf:"bytes,1,rep,name=attributes,proto3" json:"attributes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AttributeList) Reset() { *m = AttributeList{} } +func (m *AttributeList) String() string { return proto.CompactTextString(m) } +func (*AttributeList) ProtoMessage() {} +func (*AttributeList) Descriptor() ([]byte, []int) { + return fileDescriptor_e23ec92774814a82, []int{2} +} + +func (m *AttributeList) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AttributeList.Unmarshal(m, b) +} +func (m *AttributeList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AttributeList.Marshal(b, m, deterministic) +} +func (m *AttributeList) XXX_Merge(src proto.Message) { + xxx_messageInfo_AttributeList.Merge(m, src) +} +func (m *AttributeList) XXX_Size() int { + return xxx_messageInfo_AttributeList.Size(m) +} +func (m *AttributeList) XXX_DiscardUnknown() { + xxx_messageInfo_AttributeList.DiscardUnknown(m) +} + +var xxx_messageInfo_AttributeList proto.InternalMessageInfo + +func (m *AttributeList) GetAttributes() []*Attribute { + if m != nil { + return m.Attributes + } + return nil +} + +func init() { + proto.RegisterType((*AttributeAndId)(nil), "attrpubapi_v1.AttributeAndId") + proto.RegisterType((*AttributeAndIdList)(nil), "attrpubapi_v1.AttributeAndIdList") + proto.RegisterType((*AttributeList)(nil), "attrpubapi_v1.AttributeList") +} + +func init() { proto.RegisterFile("List.proto", fileDescriptor_e23ec92774814a82) } + +var fileDescriptor_e23ec92774814a82 = []byte{ + // 219 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xf2, 0xc9, 0x2c, 0x2e, + 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x4d, 0x2c, 0x29, 0x29, 0x2a, 0x28, 0x4d, 0x4a, + 0x2c, 0xc8, 0x8c, 0x2f, 0x33, 0x94, 0xe2, 0x77, 0x2c, 0x29, 0x29, 0xca, 0x4c, 0x2a, 0x2d, 0x49, + 0x85, 0xc8, 0x2b, 0x65, 0x73, 0xf1, 0xc1, 0x85, 0x1c, 0xf3, 0x52, 0x3c, 0x53, 0x84, 0xcc, 0xb8, + 0x38, 0x13, 0x61, 0x22, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xdc, 0x46, 0x12, 0x7a, 0x28, 0xa6, 0xe8, + 0xc1, 0x75, 0x04, 0x21, 0x94, 0x0a, 0x29, 0x72, 0xf1, 0xc0, 0x39, 0xf1, 0x99, 0x29, 0x12, 0x4c, + 0x0a, 0x8c, 0x1a, 0x3c, 0x41, 0xdc, 0x70, 0x31, 0xcf, 0x14, 0xa5, 0x34, 0x2e, 0x21, 0x54, 0xcb, + 0x40, 0x0e, 0x15, 0x0a, 0xe0, 0x12, 0x45, 0x68, 0x4c, 0xcc, 0x4b, 0x89, 0xcf, 0x4c, 0x89, 0xcf, + 0xc9, 0x2c, 0x2e, 0x91, 0x60, 0x54, 0x60, 0xd6, 0xe0, 0x36, 0x92, 0xc5, 0x65, 0x39, 0xd8, 0x84, + 0x20, 0xa1, 0x44, 0x0c, 0x13, 0x95, 0x3c, 0xb9, 0x78, 0xe1, 0xaa, 0xc0, 0x56, 0x58, 0x70, 0x71, + 0xc1, 0x95, 0x15, 0x43, 0xcd, 0xc5, 0xed, 0x29, 0x24, 0xb5, 0x4e, 0x46, 0x5c, 0x62, 0xc9, 0xf9, + 0xb9, 0x7a, 0x95, 0xf9, 0x25, 0x99, 0x28, 0xea, 0x8d, 0x9d, 0x38, 0x41, 0x1a, 0x02, 0x40, 0x81, + 0x18, 0xc5, 0x0b, 0x92, 0x06, 0x87, 0x27, 0x48, 0x49, 0x12, 0x1b, 0x98, 0x69, 0x0c, 0x08, 0x00, + 0x00, 0xff, 0xff, 0xc9, 0xf6, 0x43, 0xa8, 0x88, 0x01, 0x00, 0x00, +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/List.proto b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/List.proto new file mode 100644 index 0000000..9d26274 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/List.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package attrpubapi_v1; + +import "Attribute.proto"; + +option java_package = "com.yoti.api.client.spi.remote.proto"; +option java_outer_classname = "AttrProto"; + +option go_package = "yotiprotoattr"; + +// AttributeAndId is a simple container for holding an attribute's value +// alongside its ID. +message AttributeAndId { + Attribute attribute = 1; + + bytes attribute_id = 2; +} + + +message AttributeAndIdList{ + repeated AttributeAndId attribute_and_id_list = 1; +} + + +message AttributeList { + repeated Attribute attributes = 1; +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Signing.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Signing.pb.go new file mode 100644 index 0000000..a283849 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Signing.pb.go @@ -0,0 +1,127 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: Signing.proto + +package yotiprotoattr + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type AttributeSigning struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + ContentType ContentType `protobuf:"varint,3,opt,name=content_type,json=contentType,proto3,enum=attrpubapi_v1.ContentType" json:"content_type,omitempty"` + ArtifactSignature []byte `protobuf:"bytes,4,opt,name=artifact_signature,json=artifactSignature,proto3" json:"artifact_signature,omitempty"` + SubType string `protobuf:"bytes,5,opt,name=sub_type,json=subType,proto3" json:"sub_type,omitempty"` + SignedTimeStamp []byte `protobuf:"bytes,6,opt,name=signed_time_stamp,json=signedTimeStamp,proto3" json:"signed_time_stamp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AttributeSigning) Reset() { *m = AttributeSigning{} } +func (m *AttributeSigning) String() string { return proto.CompactTextString(m) } +func (*AttributeSigning) ProtoMessage() {} +func (*AttributeSigning) Descriptor() ([]byte, []int) { + return fileDescriptor_c19542824c3e34e0, []int{0} +} + +func (m *AttributeSigning) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AttributeSigning.Unmarshal(m, b) +} +func (m *AttributeSigning) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AttributeSigning.Marshal(b, m, deterministic) +} +func (m *AttributeSigning) XXX_Merge(src proto.Message) { + xxx_messageInfo_AttributeSigning.Merge(m, src) +} +func (m *AttributeSigning) XXX_Size() int { + return xxx_messageInfo_AttributeSigning.Size(m) +} +func (m *AttributeSigning) XXX_DiscardUnknown() { + xxx_messageInfo_AttributeSigning.DiscardUnknown(m) +} + +var xxx_messageInfo_AttributeSigning proto.InternalMessageInfo + +func (m *AttributeSigning) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *AttributeSigning) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func (m *AttributeSigning) GetContentType() ContentType { + if m != nil { + return m.ContentType + } + return ContentType_UNDEFINED +} + +func (m *AttributeSigning) GetArtifactSignature() []byte { + if m != nil { + return m.ArtifactSignature + } + return nil +} + +func (m *AttributeSigning) GetSubType() string { + if m != nil { + return m.SubType + } + return "" +} + +func (m *AttributeSigning) GetSignedTimeStamp() []byte { + if m != nil { + return m.SignedTimeStamp + } + return nil +} + +func init() { + proto.RegisterType((*AttributeSigning)(nil), "attrpubapi_v1.AttributeSigning") +} + +func init() { proto.RegisterFile("Signing.proto", fileDescriptor_c19542824c3e34e0) } + +var fileDescriptor_c19542824c3e34e0 = []byte{ + // 260 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x90, 0xcd, 0x4e, 0xeb, 0x30, + 0x10, 0x85, 0x95, 0x7b, 0xdb, 0x42, 0x4d, 0x43, 0xa9, 0x85, 0x50, 0xe8, 0x2a, 0x62, 0x15, 0x21, + 0x11, 0x89, 0x76, 0xcd, 0x82, 0xf2, 0x02, 0x28, 0xe9, 0x8a, 0x8d, 0x65, 0x87, 0x21, 0xb2, 0x84, + 0x7f, 0xe4, 0x8c, 0x2b, 0xe5, 0xb1, 0x79, 0x03, 0x64, 0x9b, 0x82, 0xba, 0x9b, 0x99, 0x6f, 0xe6, + 0x9c, 0xd1, 0x21, 0x79, 0x2b, 0x7b, 0x2d, 0x75, 0x5f, 0x5b, 0x67, 0xd0, 0xd0, 0x9c, 0x23, 0x3a, + 0xeb, 0x05, 0xb7, 0x92, 0x1d, 0x1e, 0xd7, 0xcb, 0x67, 0x44, 0x27, 0x85, 0x47, 0x48, 0xfc, 0xee, + 0x2b, 0x23, 0x57, 0xbf, 0xb3, 0x9f, 0x53, 0x4a, 0xc9, 0x44, 0x73, 0x05, 0x45, 0x56, 0x66, 0xd5, + 0xbc, 0x89, 0x35, 0xbd, 0x26, 0xd3, 0x03, 0xff, 0xf4, 0x50, 0xfc, 0x2b, 0xb3, 0x6a, 0xd1, 0xa4, + 0x86, 0x3e, 0x91, 0x45, 0x67, 0x34, 0x82, 0x46, 0x86, 0xa3, 0x85, 0xe2, 0x7f, 0x99, 0x55, 0x97, + 0x9b, 0x75, 0x7d, 0xe2, 0x5a, 0xbf, 0xa4, 0x95, 0xfd, 0x68, 0xa1, 0xb9, 0xe8, 0xfe, 0x1a, 0xfa, + 0x40, 0x28, 0x77, 0x28, 0x3f, 0x78, 0x87, 0x6c, 0x90, 0xbd, 0xe6, 0xe8, 0x1d, 0x14, 0x93, 0xe8, + 0xb0, 0x3a, 0x92, 0xf6, 0x08, 0xe8, 0x2d, 0x39, 0x1f, 0xbc, 0x48, 0x4e, 0xd3, 0xf8, 0xdb, 0xd9, + 0xe0, 0x45, 0x54, 0xba, 0x27, 0xab, 0x20, 0x00, 0xef, 0x0c, 0xa5, 0x02, 0x36, 0x20, 0x57, 0xb6, + 0x98, 0x45, 0xa1, 0x65, 0x02, 0x7b, 0xa9, 0xa0, 0x0d, 0xe3, 0xdd, 0x86, 0xdc, 0x74, 0x46, 0xd5, + 0xa3, 0x41, 0x79, 0xf2, 0xe8, 0x76, 0x37, 0x0f, 0x51, 0xbc, 0x86, 0x60, 0xde, 0xf2, 0x80, 0x63, + 0x46, 0x61, 0x45, 0xcc, 0x62, 0xb9, 0xfd, 0x0e, 0x00, 0x00, 0xff, 0xff, 0xed, 0xfc, 0x51, 0x61, + 0x5f, 0x01, 0x00, 0x00, +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Signing.proto b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Signing.proto new file mode 100644 index 0000000..67d4e5d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotoattr/Signing.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package attrpubapi_v1; + +import "Attribute.proto"; + +option java_package = "com.yoti.api.client.spi.remote.proto"; +option java_outer_classname = "AttrProto"; + +option go_package = "yotiprotoattr"; + +message AttributeSigning { + string name = 1; + + bytes value = 2; + + ContentType content_type = 3; + + bytes artifact_signature = 4; + + string sub_type = 5; + + bytes signed_time_stamp = 6; +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/EncryptedData.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/EncryptedData.pb.go new file mode 100644 index 0000000..7de16b8 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/EncryptedData.pb.go @@ -0,0 +1,91 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: EncryptedData.proto + +package yotiprotocom + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type EncryptedData struct { + // the iv will be used in conjunction with the secret key + // received via other channel in order to decrypt the cipher_text + Iv []byte `protobuf:"bytes,1,opt,name=iv,proto3" json:"iv,omitempty"` + // block of bytes to be decrypted + CipherText []byte `protobuf:"bytes,2,opt,name=cipher_text,json=cipherText,proto3" json:"cipher_text,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EncryptedData) Reset() { *m = EncryptedData{} } +func (m *EncryptedData) String() string { return proto.CompactTextString(m) } +func (*EncryptedData) ProtoMessage() {} +func (*EncryptedData) Descriptor() ([]byte, []int) { + return fileDescriptor_ed1db0e32f8afa88, []int{0} +} + +func (m *EncryptedData) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EncryptedData.Unmarshal(m, b) +} +func (m *EncryptedData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EncryptedData.Marshal(b, m, deterministic) +} +func (m *EncryptedData) XXX_Merge(src proto.Message) { + xxx_messageInfo_EncryptedData.Merge(m, src) +} +func (m *EncryptedData) XXX_Size() int { + return xxx_messageInfo_EncryptedData.Size(m) +} +func (m *EncryptedData) XXX_DiscardUnknown() { + xxx_messageInfo_EncryptedData.DiscardUnknown(m) +} + +var xxx_messageInfo_EncryptedData proto.InternalMessageInfo + +func (m *EncryptedData) GetIv() []byte { + if m != nil { + return m.Iv + } + return nil +} + +func (m *EncryptedData) GetCipherText() []byte { + if m != nil { + return m.CipherText + } + return nil +} + +func init() { + proto.RegisterType((*EncryptedData)(nil), "compubapi_v1.EncryptedData") +} + +func init() { proto.RegisterFile("EncryptedData.proto", fileDescriptor_ed1db0e32f8afa88) } + +var fileDescriptor_ed1db0e32f8afa88 = []byte{ + // 145 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x76, 0xcd, 0x4b, 0x2e, + 0xaa, 0x2c, 0x28, 0x49, 0x4d, 0x71, 0x49, 0x2c, 0x49, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, + 0xe2, 0x49, 0xce, 0xcf, 0x2d, 0x28, 0x4d, 0x4a, 0x2c, 0xc8, 0x8c, 0x2f, 0x33, 0x54, 0x72, 0xe0, + 0xe2, 0x45, 0x51, 0x24, 0xc4, 0xc7, 0xc5, 0x94, 0x59, 0x26, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x13, + 0xc4, 0x94, 0x59, 0x26, 0x24, 0xcf, 0xc5, 0x9d, 0x9c, 0x59, 0x90, 0x91, 0x5a, 0x14, 0x5f, 0x92, + 0x5a, 0x51, 0x22, 0xc1, 0x04, 0x96, 0xe0, 0x82, 0x08, 0x85, 0xa4, 0x56, 0x94, 0x38, 0x59, 0x72, + 0x89, 0x26, 0xe7, 0xe7, 0xea, 0x55, 0xe6, 0x97, 0x64, 0xea, 0x21, 0x19, 0x6d, 0xec, 0x24, 0x84, + 0x62, 0x70, 0x00, 0xc8, 0xf2, 0x28, 0x1e, 0x90, 0x32, 0xb0, 0x3b, 0x92, 0xf3, 0x73, 0x93, 0xd8, + 0xc0, 0x2c, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x0a, 0xe9, 0x2c, 0x57, 0xa8, 0x00, 0x00, + 0x00, +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/EncryptedData.proto b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/EncryptedData.proto new file mode 100644 index 0000000..d9f1e4d --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/EncryptedData.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package compubapi_v1; + +option java_package = "com.yoti.api.client.spi.remote.proto"; +option java_outer_classname = "EncryptedDataProto"; + +option go_package = "yotiprotocom"; + +message EncryptedData { + // the iv will be used in conjunction with the secret key + // received via other channel in order to decrypt the cipher_text + bytes iv = 1; + + // block of bytes to be decrypted + bytes cipher_text = 2; +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/SignedTimestamp.pb.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/SignedTimestamp.pb.go new file mode 100644 index 0000000..180ec47 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/SignedTimestamp.pb.go @@ -0,0 +1,147 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: SignedTimestamp.proto + +package yotiprotocom + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// SignedTimestamp is a timestamp associated with a message that has a +// cryptographic signature proving that it was issued by the correct authority. +type SignedTimestamp struct { + // Version indicates how the digests within this object are calculated. + Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + // Timestamp is the time this SignedTimestamp was issued. It is in UTC, + // as µseconds elapsed since the epoch (µs from 1970-01-01T00:00:00Z). + Timestamp uint64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // MessageDigest is the digest of the message this timestamp is + // associated with. The first step in verifying the timestamp is + // ensuring the MessageDigest matches the original message data. + // + // For version 1 objects, the message digest algorithm is SHA-512/224. + MessageDigest []byte `protobuf:"bytes,3,opt,name=message_digest,json=messageDigest,proto3" json:"message_digest,omitempty"` + // ChainDigest is the digest of the previous SignedTimestamp message + // in the chain. The second step in verifying the timestamp is walking + // back over the chain and checking each SignedTimestamp's ChainDigest + // field. The SignedTimestamp at the beginning of the chain has this + // field set to a specific, publish value. + // + // For version 1 objects, the chain digest algorithm is HMAC-SHA-512/224, + // with the secret being equal to the MessageDigest field. + ChainDigest []byte `protobuf:"bytes,4,opt,name=chain_digest,json=chainDigest,proto3" json:"chain_digest,omitempty"` + // ChainDigestSkip1 is only populated once every 500 nodes. It is the + // ChainDigest value of the timestamp 500 nodes previously. + ChainDigestSkip1 []byte `protobuf:"bytes,5,opt,name=chain_digest_skip1,json=chainDigestSkip1,proto3" json:"chain_digest_skip1,omitempty"` + // ChainDigestSkip2 is only populated once every 250000 nodes (or once + // every 500 nodes that have ChainDigestSkip1 populated). It is the + // ChainDigest value of the timestamp 250000 nodes previously. + ChainDigestSkip2 []byte `protobuf:"bytes,6,opt,name=chain_digest_skip2,json=chainDigestSkip2,proto3" json:"chain_digest_skip2,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SignedTimestamp) Reset() { *m = SignedTimestamp{} } +func (m *SignedTimestamp) String() string { return proto.CompactTextString(m) } +func (*SignedTimestamp) ProtoMessage() {} +func (*SignedTimestamp) Descriptor() ([]byte, []int) { + return fileDescriptor_5602a15d48f50e63, []int{0} +} + +func (m *SignedTimestamp) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SignedTimestamp.Unmarshal(m, b) +} +func (m *SignedTimestamp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SignedTimestamp.Marshal(b, m, deterministic) +} +func (m *SignedTimestamp) XXX_Merge(src proto.Message) { + xxx_messageInfo_SignedTimestamp.Merge(m, src) +} +func (m *SignedTimestamp) XXX_Size() int { + return xxx_messageInfo_SignedTimestamp.Size(m) +} +func (m *SignedTimestamp) XXX_DiscardUnknown() { + xxx_messageInfo_SignedTimestamp.DiscardUnknown(m) +} + +var xxx_messageInfo_SignedTimestamp proto.InternalMessageInfo + +func (m *SignedTimestamp) GetVersion() int32 { + if m != nil { + return m.Version + } + return 0 +} + +func (m *SignedTimestamp) GetTimestamp() uint64 { + if m != nil { + return m.Timestamp + } + return 0 +} + +func (m *SignedTimestamp) GetMessageDigest() []byte { + if m != nil { + return m.MessageDigest + } + return nil +} + +func (m *SignedTimestamp) GetChainDigest() []byte { + if m != nil { + return m.ChainDigest + } + return nil +} + +func (m *SignedTimestamp) GetChainDigestSkip1() []byte { + if m != nil { + return m.ChainDigestSkip1 + } + return nil +} + +func (m *SignedTimestamp) GetChainDigestSkip2() []byte { + if m != nil { + return m.ChainDigestSkip2 + } + return nil +} + +func init() { + proto.RegisterType((*SignedTimestamp)(nil), "compubapi_v1.SignedTimestamp") +} + +func init() { proto.RegisterFile("SignedTimestamp.proto", fileDescriptor_5602a15d48f50e63) } + +var fileDescriptor_5602a15d48f50e63 = []byte{ + // 222 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x0d, 0xce, 0x4c, 0xcf, + 0x4b, 0x4d, 0x09, 0xc9, 0xcc, 0x4d, 0x2d, 0x2e, 0x49, 0xcc, 0x2d, 0xd0, 0x2b, 0x28, 0xca, 0x2f, + 0xc9, 0x17, 0xe2, 0x49, 0xce, 0xcf, 0x2d, 0x28, 0x4d, 0x4a, 0x2c, 0xc8, 0x8c, 0x2f, 0x33, 0x54, + 0x7a, 0xcf, 0xc8, 0xc5, 0x8f, 0xa6, 0x4e, 0x48, 0x82, 0x8b, 0xbd, 0x2c, 0xb5, 0xa8, 0x38, 0x33, + 0x3f, 0x4f, 0x82, 0x51, 0x81, 0x51, 0x83, 0x35, 0x08, 0xc6, 0x15, 0x92, 0xe1, 0xe2, 0x2c, 0x81, + 0x29, 0x93, 0x60, 0x52, 0x60, 0xd4, 0x60, 0x09, 0x42, 0x08, 0x08, 0xa9, 0x72, 0xf1, 0xe5, 0xa6, + 0x16, 0x17, 0x27, 0xa6, 0xa7, 0xc6, 0xa7, 0x64, 0xa6, 0xa7, 0x16, 0x97, 0x48, 0x30, 0x2b, 0x30, + 0x6a, 0xf0, 0x04, 0xf1, 0x42, 0x45, 0x5d, 0xc0, 0x82, 0x42, 0x8a, 0x5c, 0x3c, 0xc9, 0x19, 0x89, + 0x99, 0x79, 0x30, 0x45, 0x2c, 0x60, 0x45, 0xdc, 0x60, 0x31, 0xa8, 0x12, 0x1d, 0x2e, 0x21, 0x64, + 0x25, 0xf1, 0xc5, 0xd9, 0x99, 0x05, 0x86, 0x12, 0xac, 0x60, 0x85, 0x02, 0x48, 0x0a, 0x83, 0x41, + 0xe2, 0x58, 0x55, 0x1b, 0x49, 0xb0, 0x61, 0x55, 0x6d, 0xe4, 0x64, 0xcd, 0x25, 0x9a, 0x9c, 0x9f, + 0xab, 0x57, 0x99, 0x5f, 0x92, 0xa9, 0x87, 0x14, 0x14, 0xc6, 0x4e, 0x22, 0x68, 0xe1, 0x10, 0x00, + 0x0a, 0xae, 0x28, 0x1e, 0x90, 0x42, 0x70, 0xc8, 0x25, 0xe7, 0xe7, 0x26, 0xb1, 0x81, 0x59, 0xc6, + 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa1, 0x27, 0xbf, 0x01, 0x5c, 0x01, 0x00, 0x00, +} diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/SignedTimestamp.proto b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/SignedTimestamp.proto new file mode 100644 index 0000000..1629722 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiprotocom/SignedTimestamp.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package compubapi_v1; + +option java_package = "com.yoti.api.client.spi.remote.proto"; +option java_outer_classname = "SignedTimestampProto"; + +option go_package = "yotiprotocom"; + +// SignedTimestamp is a timestamp associated with a message that has a +// cryptographic signature proving that it was issued by the correct authority. +message SignedTimestamp { + // Version indicates how the digests within this object are calculated. + int32 version = 1; + + // Timestamp is the time this SignedTimestamp was issued. It is in UTC, + // as µseconds elapsed since the epoch (µs from 1970-01-01T00:00:00Z). + uint64 timestamp = 2; + + // MessageDigest is the digest of the message this timestamp is + // associated with. The first step in verifying the timestamp is + // ensuring the MessageDigest matches the original message data. + // + // For version 1 objects, the message digest algorithm is SHA-512/224. + bytes message_digest = 3; + + // ChainDigest is the digest of the previous SignedTimestamp message + // in the chain. The second step in verifying the timestamp is walking + // back over the chain and checking each SignedTimestamp's ChainDigest + // field. The SignedTimestamp at the beginning of the chain has this + // field set to a specific, publish value. + // + // For version 1 objects, the chain digest algorithm is HMAC-SHA-512/224, + // with the secret being equal to the MessageDigest field. + bytes chain_digest = 4; + + // ChainDigestSkip1 is only populated once every 500 nodes. It is the + // ChainDigest value of the timestamp 500 nodes previously. + bytes chain_digest_skip1 = 5; + + // ChainDigestSkip2 is only populated once every 250000 nodes (or once + // every 500 nodes that have ChainDigestSkip1 populated). It is the + // ChainDigest value of the timestamp 250000 nodes previously. + bytes chain_digest_skip2 = 6; +} \ No newline at end of file diff --git a/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiuserprofile.go b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiuserprofile.go new file mode 100644 index 0000000..2091586 --- /dev/null +++ b/root/pkg/mod/github.com/getyoti/yoti-go-sdk@v2.3.1+incompatible/yotiuserprofile.go @@ -0,0 +1,53 @@ +package yoti + +import ( + "time" +) + +// Deprecated: Will be removed in v3.0.0. Use `Profile` instead. UserProfile represents the details retrieved for a particular +type UserProfile struct { + // ID is a unique identifier Yoti assigns to your user, but only for your app. + // If the same user logs into your app again, you get the same id. + // If she/he logs into another application, Yoti will assign a different id for that app. + ID string + + // Selfie is a photograph of the user. This will be nil if not provided by Yoti + Selfie *Image + + // GivenNames represents the user's given names. This will be an empty string if not provided by Yoti + GivenNames string + + // Family represents the user's family name. This will be an empty string if not provided by Yoti + FamilyName string + + // Full name represents the user's full name. This will be an empty string if not provided by Yoti + FullName string + + // MobileNumber represents the user's mobile phone number. This will be an empty string if not provided by Yoti + MobileNumber string + + // EmailAddress represents the user's email address. This will be an empty string if not provided by Yoti + EmailAddress string + + // DateOfBirth represents the user's date of birth. This will be nil if not provided by Yoti + DateOfBirth *time.Time + + // IsAgeVerified represents the result of the age verification check on the user. The bool will be true if they passed, false if they failed, and nil if there was no check + IsAgeVerified *bool + + // Address represents the user's address. This will be an empty string if not provided by Yoti + Address string + + // StructuredPostalAddress represents the user's address in a JSON format. This will be empty if not provided by Yoti + StructuredPostalAddress interface{} + + // Gender represents the user's gender. This will be an empty string if not provided by Yoti + Gender string + + // Nationality represents the user's nationality. This will be an empty string if not provided by Yoti + Nationality string + + // OtherAttributes is a map of any other information about the user provided by Yoti. The key will be the name + // of the piece of information, and the keys associated value will be the piece of information itself. + OtherAttributes map[string]AttributeValue +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/.github/workflows/test.yml b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/.github/workflows/test.yml new file mode 100644 index 0000000..6b1b1c4 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/.github/workflows/test.yml @@ -0,0 +1,30 @@ +on: [push, pull_request] +name: Test +jobs: + test: + env: + GOPATH: ${{ github.workspace }} + defaults: + run: + working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} + strategy: + matrix: + go-version: [1.8.x, 1.9.x, 1.10.x, 1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + with: + path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} + - name: Checkout dependencies + run: go get golang.org/x/xerrors + - name: Test + run: go test -v -race ./... + - name: Format + if: matrix.go-version == '1.16.x' + run: diff -u <(echo -n) <(gofmt -d .) diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/CONTRIBUTING.md b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/CONTRIBUTING.md new file mode 100644 index 0000000..ae319c7 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/LICENSE b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/LICENSE new file mode 100644 index 0000000..32017f8 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/README.md b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/README.md new file mode 100644 index 0000000..ed0eb9b --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/README.md @@ -0,0 +1,44 @@ +# Package for equality of Go values + +[![GoDev](https://img.shields.io/static/v1?label=godev&message=reference&color=00add8)][godev] +[![Build Status](https://travis-ci.org/google/go-cmp.svg?branch=master)][travis] + +This package is intended to be a more powerful and safer alternative to +`reflect.DeepEqual` for comparing whether two values are semantically equal. + +The primary features of `cmp` are: + +* When the default behavior of equality does not suit the needs of the test, + custom equality functions can override the equality operation. + For example, an equality function may report floats as equal so long as they + are within some tolerance of each other. + +* Types that have an `Equal` method may use that method to determine equality. + This allows package authors to determine the equality operation for the types + that they define. + +* If no custom equality functions are used and no `Equal` method is defined, + equality is determined by recursively comparing the primitive kinds on both + values, much like `reflect.DeepEqual`. Unlike `reflect.DeepEqual`, unexported + fields are not compared by default; they result in panics unless suppressed + by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explicitly + compared using the `AllowUnexported` option. + +See the [documentation][godev] for more information. + +This is not an official Google product. + +[godev]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[travis]: https://travis-ci.org/google/go-cmp + +## Install + +``` +go get -u github.com/google/go-cmp/cmp +``` + +## License + +BSD - See [LICENSE][license] file + +[license]: https://github.com/google/go-cmp/blob/master/LICENSE diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/equate.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/equate.go new file mode 100644 index 0000000..e4ffca8 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/equate.go @@ -0,0 +1,148 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cmpopts provides common options for the cmp package. +package cmpopts + +import ( + "math" + "reflect" + "time" + + "github.com/google/go-cmp/cmp" +) + +func equateAlways(_, _ interface{}) bool { return true } + +// EquateEmpty returns a Comparer option that determines all maps and slices +// with a length of zero to be equal, regardless of whether they are nil. +// +// EquateEmpty can be used in conjunction with SortSlices and SortMaps. +func EquateEmpty() cmp.Option { + return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways)) +} + +func isEmpty(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) +} + +// EquateApprox returns a Comparer option that determines float32 or float64 +// values to be equal if they are within a relative fraction or absolute margin. +// This option is not used when either x or y is NaN or infinite. +// +// The fraction determines that the difference of two values must be within the +// smaller fraction of the two values, while the margin determines that the two +// values must be within some absolute margin. +// To express only a fraction or only a margin, use 0 for the other parameter. +// The fraction and margin must be non-negative. +// +// The mathematical expression used is equivalent to: +// |x-y| ≤ max(fraction*min(|x|, |y|), margin) +// +// EquateApprox can be used in conjunction with EquateNaNs. +func EquateApprox(fraction, margin float64) cmp.Option { + if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) { + panic("margin or fraction must be a non-negative number") + } + a := approximator{fraction, margin} + return cmp.Options{ + cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)), + cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)), + } +} + +type approximator struct{ frac, marg float64 } + +func areRealF64s(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) +} +func areRealF32s(x, y float32) bool { + return areRealF64s(float64(x), float64(y)) +} +func (a approximator) compareF64(x, y float64) bool { + relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y)) + return math.Abs(x-y) <= math.Max(a.marg, relMarg) +} +func (a approximator) compareF32(x, y float32) bool { + return a.compareF64(float64(x), float64(y)) +} + +// EquateNaNs returns a Comparer option that determines float32 and float64 +// NaN values to be equal. +// +// EquateNaNs can be used in conjunction with EquateApprox. +func EquateNaNs() cmp.Option { + return cmp.Options{ + cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)), + cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)), + } +} + +func areNaNsF64s(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) +} +func areNaNsF32s(x, y float32) bool { + return areNaNsF64s(float64(x), float64(y)) +} + +// EquateApproxTime returns a Comparer option that determines two non-zero +// time.Time values to be equal if they are within some margin of one another. +// If both times have a monotonic clock reading, then the monotonic time +// difference will be used. The margin must be non-negative. +func EquateApproxTime(margin time.Duration) cmp.Option { + if margin < 0 { + panic("margin must be a non-negative number") + } + a := timeApproximator{margin} + return cmp.FilterValues(areNonZeroTimes, cmp.Comparer(a.compare)) +} + +func areNonZeroTimes(x, y time.Time) bool { + return !x.IsZero() && !y.IsZero() +} + +type timeApproximator struct { + margin time.Duration +} + +func (a timeApproximator) compare(x, y time.Time) bool { + // Avoid subtracting times to avoid overflow when the + // difference is larger than the largest representible duration. + if x.After(y) { + // Ensure x is always before y + x, y = y, x + } + // We're within the margin if x+margin >= y. + // Note: time.Time doesn't have AfterOrEqual method hence the negation. + return !x.Add(a.margin).Before(y) +} + +// AnyError is an error that matches any non-nil error. +var AnyError anyError + +type anyError struct{} + +func (anyError) Error() string { return "any error" } +func (anyError) Is(err error) bool { return err != nil } + +// EquateErrors returns a Comparer option that determines errors to be equal +// if errors.Is reports them to match. The AnyError error can be used to +// match any non-nil error. +func EquateErrors() cmp.Option { + return cmp.FilterValues(areConcreteErrors, cmp.Comparer(compareErrors)) +} + +// areConcreteErrors reports whether x and y are types that implement error. +// The input types are deliberately of the interface{} type rather than the +// error type so that we can handle situations where the current type is an +// interface{}, but the underlying concrete types both happen to implement +// the error interface. +func areConcreteErrors(x, y interface{}) bool { + _, ok1 := x.(error) + _, ok2 := y.(error) + return ok1 && ok2 +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/errors_go113.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/errors_go113.go new file mode 100644 index 0000000..26fe25d --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/errors_go113.go @@ -0,0 +1,15 @@ +// Copyright 2021, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.13 + +package cmpopts + +import "errors" + +func compareErrors(x, y interface{}) bool { + xe := x.(error) + ye := y.(error) + return errors.Is(xe, ye) || errors.Is(ye, xe) +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/errors_xerrors.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/errors_xerrors.go new file mode 100644 index 0000000..6eeb8d6 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/errors_xerrors.go @@ -0,0 +1,18 @@ +// Copyright 2021, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.13 + +// TODO(≥go1.13): For support on = 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) { + panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i))) + } + start = -1 + } else if start == -1 { + start = i + } + } +} +func (ss sliceSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i), v.Index(j) + return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} + +// SortMaps returns a Transformer option that flattens map[K]V types to be a +// sorted []struct{K, V}. The less function must be of the form +// "func(T, T) bool" which is used to sort any map with key K that is +// assignable to T. +// +// Flattening the map into a slice has the property that cmp.Equal is able to +// use Comparers on K or the K.Equal method if it exists. +// +// The less function must be: +// • Deterministic: less(x, y) == less(x, y) +// • Irreflexive: !less(x, x) +// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// • Total: if x != y, then either less(x, y) or less(y, x) +// +// SortMaps can be used in conjunction with EquateEmpty. +func SortMaps(lessFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessFunc) + if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { + panic(fmt.Sprintf("invalid less function: %T", lessFunc)) + } + ms := mapSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort)) +} + +type mapSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ms mapSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) && + (vx.Len() != 0 || vy.Len() != 0) +} +func (ms mapSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + outType := reflect.StructOf([]reflect.StructField{ + {Name: "K", Type: src.Type().Key()}, + {Name: "V", Type: src.Type().Elem()}, + }) + dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len()) + for i, k := range src.MapKeys() { + v := reflect.New(outType).Elem() + v.Field(0).Set(k) + v.Field(1).Set(src.MapIndex(k)) + dst.Index(i).Set(v) + } + sort.Slice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) }) + ms.checkSort(dst) + return dst.Interface() +} +func (ms mapSorter) checkSort(v reflect.Value) { + for i := 1; i < v.Len(); i++ { + if !ms.less(v, i-1, i) { + panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i))) + } + } +} +func (ms mapSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i).Field(0), v.Index(j).Field(0) + return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/struct_filter.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/struct_filter.go new file mode 100644 index 0000000..a09829c --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/struct_filter.go @@ -0,0 +1,187 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// filterField returns a new Option where opt is only evaluated on paths that +// include a specific exported field on a single struct type. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a +// specific sub-field that is embedded or nested within the parent struct. +func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option { + // TODO: This is currently unexported over concerns of how helper filters + // can be composed together easily. + // TODO: Add tests for FilterField. + + sf := newStructFilter(typ, name) + return cmp.FilterPath(sf.filter, opt) +} + +type structFilter struct { + t reflect.Type // The root struct type to match on + ft fieldTree // Tree of fields to match on +} + +func newStructFilter(typ interface{}, names ...string) structFilter { + // TODO: Perhaps allow * as a special identifier to allow ignoring any + // number of path steps until the next field match? + // This could be useful when a concrete struct gets transformed into + // an anonymous struct where it is not possible to specify that by type, + // but the transformer happens to provide guarantees about the names of + // the transformed fields. + + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) + } + var ft fieldTree + for _, name := range names { + cname, err := canonicalName(t, name) + if err != nil { + panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err)) + } + ft.insert(cname) + } + return structFilter{t, ft} +} + +func (sf structFilter) filter(p cmp.Path) bool { + for i, ps := range p { + if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) { + return true + } + } + return false +} + +// fieldTree represents a set of dot-separated identifiers. +// +// For example, inserting the following selectors: +// Foo +// Foo.Bar.Baz +// Foo.Buzz +// Nuka.Cola.Quantum +// +// Results in a tree of the form: +// {sub: { +// "Foo": {ok: true, sub: { +// "Bar": {sub: { +// "Baz": {ok: true}, +// }}, +// "Buzz": {ok: true}, +// }}, +// "Nuka": {sub: { +// "Cola": {sub: { +// "Quantum": {ok: true}, +// }}, +// }}, +// }} +type fieldTree struct { + ok bool // Whether this is a specified node + sub map[string]fieldTree // The sub-tree of fields under this node +} + +// insert inserts a sequence of field accesses into the tree. +func (ft *fieldTree) insert(cname []string) { + if ft.sub == nil { + ft.sub = make(map[string]fieldTree) + } + if len(cname) == 0 { + ft.ok = true + return + } + sub := ft.sub[cname[0]] + sub.insert(cname[1:]) + ft.sub[cname[0]] = sub +} + +// matchPrefix reports whether any selector in the fieldTree matches +// the start of path p. +func (ft fieldTree) matchPrefix(p cmp.Path) bool { + for _, ps := range p { + switch ps := ps.(type) { + case cmp.StructField: + ft = ft.sub[ps.Name()] + if ft.ok { + return true + } + if len(ft.sub) == 0 { + return false + } + case cmp.Indirect: + default: + return false + } + } + return false +} + +// canonicalName returns a list of identifiers where any struct field access +// through an embedded field is expanded to include the names of the embedded +// types themselves. +// +// For example, suppose field "Foo" is not directly in the parent struct, +// but actually from an embedded struct of type "Bar". Then, the canonical name +// of "Foo" is actually "Bar.Foo". +// +// Suppose field "Foo" is not directly in the parent struct, but actually +// a field in two different embedded structs of types "Bar" and "Baz". +// Then the selector "Foo" causes a panic since it is ambiguous which one it +// refers to. The user must specify either "Bar.Foo" or "Baz.Foo". +func canonicalName(t reflect.Type, sel string) ([]string, error) { + var name string + sel = strings.TrimPrefix(sel, ".") + if sel == "" { + return nil, fmt.Errorf("name must not be empty") + } + if i := strings.IndexByte(sel, '.'); i < 0 { + name, sel = sel, "" + } else { + name, sel = sel[:i], sel[i:] + } + + // Type must be a struct or pointer to struct. + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("%v must be a struct", t) + } + + // Find the canonical name for this current field name. + // If the field exists in an embedded struct, then it will be expanded. + sf, _ := t.FieldByName(name) + if !isExported(name) { + // Avoid using reflect.Type.FieldByName for unexported fields due to + // buggy behavior with regard to embeddeding and unexported fields. + // See https://golang.org/issue/4876 for details. + sf = reflect.StructField{} + for i := 0; i < t.NumField() && sf.Name == ""; i++ { + if t.Field(i).Name == name { + sf = t.Field(i) + } + } + } + if sf.Name == "" { + return []string{name}, fmt.Errorf("does not exist") + } + var ss []string + for i := range sf.Index { + ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name) + } + if sel == "" { + return ss, nil + } + ssPost, err := canonicalName(sf.Type, sel) + return append(ss, ssPost...), err +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/util_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/util_test.go new file mode 100644 index 0000000..b19bcab --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/util_test.go @@ -0,0 +1,1371 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" + "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/xerrors" +) + +type ( + MyInt int + MyInts []int + MyFloat float32 + MyString string + MyTime struct{ time.Time } + MyStruct struct { + A, B []int + C, D map[time.Time]string + } + + Foo1 struct{ Alpha, Bravo, Charlie int } + Foo2 struct{ *Foo1 } + Foo3 struct{ *Foo2 } + Bar1 struct{ Foo3 } + Bar2 struct { + Bar1 + *Foo3 + Bravo float32 + } + Bar3 struct { + Bar1 + Bravo *Bar2 + Delta struct{ Echo Foo1 } + *Foo3 + Alpha string + } + + privateStruct struct{ Public, private int } + PublicStruct struct{ Public, private int } + ParentStruct struct { + *privateStruct + *PublicStruct + Public int + private int + } + + Everything struct { + MyInt + MyFloat + MyTime + MyStruct + Bar3 + ParentStruct + } + + EmptyInterface interface{} +) + +func TestOptions(t *testing.T) { + createBar3X := func() *Bar3 { + return &Bar3{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 2}}}}, + Bravo: &Bar2{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 7}}}}, + Foo3: &Foo3{&Foo2{&Foo1{Bravo: 5}}}, + Bravo: 4, + }, + Delta: struct{ Echo Foo1 }{Foo1{Charlie: 3}}, + Foo3: &Foo3{&Foo2{&Foo1{Alpha: 1}}}, + Alpha: "alpha", + } + } + createBar3Y := func() *Bar3 { + return &Bar3{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 3}}}}, + Bravo: &Bar2{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 8}}}}, + Foo3: &Foo3{&Foo2{&Foo1{Bravo: 6}}}, + Bravo: 5, + }, + Delta: struct{ Echo Foo1 }{Foo1{Charlie: 4}}, + Foo3: &Foo3{&Foo2{&Foo1{Alpha: 2}}}, + Alpha: "ALPHA", + } + } + + tests := []struct { + label string // Test name + x, y interface{} // Input values to compare + opts []cmp.Option // Input options + wantEqual bool // Whether the inputs are equal + wantPanic bool // Whether Equal should panic + reason string // The reason for the expected outcome + }{{ + label: "EquateEmpty", + x: []int{}, + y: []int(nil), + wantEqual: false, + reason: "not equal because empty non-nil and nil slice differ", + }, { + label: "EquateEmpty", + x: []int{}, + y: []int(nil), + opts: []cmp.Option{EquateEmpty()}, + wantEqual: true, + reason: "equal because EquateEmpty equates empty slices", + }, { + label: "SortSlices", + x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + wantEqual: false, + reason: "not equal because element order differs", + }, { + label: "SortSlices", + x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })}, + wantEqual: true, + reason: "equal because SortSlices sorts the slices", + }, { + label: "SortSlices", + x: []MyInt{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []MyInt{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })}, + wantEqual: false, + reason: "not equal because MyInt is not the same type as int", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2}, + y: []float64{2, 0, 2, 1, 2, 1}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })}, + wantEqual: true, + reason: "equal even when sorted with duplicate elements", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })}, + wantPanic: true, + reason: "panics because SortSlices used with non-transitive less function", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { + return (!math.IsNaN(x) && math.IsNaN(y)) || x < y + })}, + wantEqual: false, + reason: "no panics because SortSlices used with valid less function; not equal because NaN != NaN", + }, { + label: "SortSlices+EquateNaNs", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, math.NaN(), 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, math.NaN(), 2}, + opts: []cmp.Option{ + EquateNaNs(), + SortSlices(func(x, y float64) bool { + return (!math.IsNaN(x) && math.IsNaN(y)) || x < y + }), + }, + wantEqual: true, + reason: "no panics because SortSlices used with valid less function; equal because EquateNaNs is used", + }, { + label: "SortMaps", + x: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday", + }, + y: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday", + }, + wantEqual: false, + reason: "not equal because timezones differ", + }, { + label: "SortMaps", + x: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday", + }, + y: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday", + }, + opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })}, + wantEqual: true, + reason: "equal because SortMaps flattens to a slice where Time.Equal can be used", + }, { + label: "SortMaps", + x: map[MyTime]string{ + {time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}: "0th birthday", + {time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC)}: "1st birthday", + {time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC)}: "2nd birthday", + }, + y: map[MyTime]string{ + {time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "0th birthday", + {time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "1st birthday", + {time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "2nd birthday", + }, + opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })}, + wantEqual: false, + reason: "not equal because MyTime is not assignable to time.Time", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, -1, -2, -3}, + y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, 100, 200, 300}, + opts: []cmp.Option{SortMaps(func(a, b int) bool { + if -10 < a && a <= 0 { + a *= -100 + } + if -10 < b && b <= 0 { + b *= -100 + } + return a < b + })}, + wantEqual: false, + reason: "not equal because values differ even though SortMap provides valid ordering", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, -1, -2, -3}, + y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, 100, 200, 300}, + opts: []cmp.Option{ + SortMaps(func(x, y int) bool { + if -10 < x && x <= 0 { + x *= -100 + } + if -10 < y && y <= 0 { + y *= -100 + } + return x < y + }), + cmp.Comparer(func(x, y int) bool { + if -10 < x && x <= 0 { + x *= -100 + } + if -10 < y && y <= 0 { + y *= -100 + } + return x == y + }), + }, + wantEqual: true, + reason: "equal because Comparer used to equate differences", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + y: map[int]string{}, + opts: []cmp.Option{SortMaps(func(x, y int) bool { + return x < y && x >= 0 && y >= 0 + })}, + wantPanic: true, + reason: "panics because SortMaps used with non-transitive less function", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + y: map[int]string{}, + opts: []cmp.Option{SortMaps(func(x, y int) bool { + return math.Abs(float64(x)) < math.Abs(float64(y)) + })}, + wantPanic: true, + reason: "panics because SortMaps used with partial less function", + }, { + label: "EquateEmpty+SortSlices+SortMaps", + x: MyStruct{ + A: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + C: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + }, + D: map[time.Time]string{}, + }, + y: MyStruct{ + A: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + B: []int{}, + C: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + }, + }, + opts: []cmp.Option{ + EquateEmpty(), + SortSlices(func(x, y int) bool { return x < y }), + SortMaps(func(x, y time.Time) bool { return x.Before(y) }), + }, + wantEqual: true, + reason: "no panics because EquateEmpty should compose with the sort options", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + wantEqual: false, + reason: "not equal because floats do not exactly matches", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: false, + reason: "not equal because EquateApprox(0 ,0) is equivalent to using ==", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.003, 0.009)}, + wantEqual: false, + reason: "not equal because EquateApprox is too strict", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0, 0.011)}, + wantEqual: true, + reason: "equal because margin is loose enough to match", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: true, + reason: "equal because fraction is loose enough to match", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.004, 0.011)}, + wantEqual: true, + reason: "equal because both the margin and fraction are loose enough to match", + }, { + label: "EquateApprox", + x: float32(3.09), + y: float64(3.10), + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: false, + reason: "not equal because the types differ", + }, { + label: "EquateApprox", + x: float32(3.09), + y: float32(3.10), + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: true, + reason: "equal because EquateApprox also applies on float32s", + }, { + label: "EquateApprox", + x: []float64{math.Inf(+1), math.Inf(-1)}, + y: []float64{math.Inf(+1), math.Inf(-1)}, + opts: []cmp.Option{EquateApprox(0, 1)}, + wantEqual: true, + reason: "equal because we fall back on == which matches Inf (EquateApprox does not apply on Inf) ", + }, { + label: "EquateApprox", + x: []float64{math.Inf(+1), -1e100}, + y: []float64{+1e100, math.Inf(-1)}, + opts: []cmp.Option{EquateApprox(0, 1)}, + wantEqual: false, + reason: "not equal because we fall back on == where Inf != 1e100 (EquateApprox does not apply on Inf)", + }, { + label: "EquateApprox", + x: float64(+1e100), + y: float64(-1e100), + opts: []cmp.Option{EquateApprox(math.Inf(+1), 0)}, + wantEqual: true, + reason: "equal because infinite fraction matches everything", + }, { + label: "EquateApprox", + x: float64(+1e100), + y: float64(-1e100), + opts: []cmp.Option{EquateApprox(0, math.Inf(+1))}, + wantEqual: true, + reason: "equal because infinite margin matches everything", + }, { + label: "EquateApprox", + x: math.Pi, + y: math.Pi, + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: true, + reason: "equal because EquateApprox(0, 0) is equivalent to ==", + }, { + label: "EquateApprox", + x: math.Pi, + y: math.Nextafter(math.Pi, math.Inf(+1)), + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: false, + reason: "not equal because EquateApprox(0, 0) is equivalent to ==", + }, { + label: "EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + wantEqual: false, + reason: "not equal because NaN != NaN", + }, { + label: "EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + opts: []cmp.Option{EquateNaNs()}, + wantEqual: true, + reason: "equal because EquateNaNs allows NaN == NaN", + }, { + label: "EquateNaNs", + x: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0}, + y: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0}, + opts: []cmp.Option{EquateNaNs()}, + wantEqual: true, + reason: "equal because EquateNaNs operates on float32", + }, { + label: "EquateApprox+EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.01, 5001}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.02, 5002}, + opts: []cmp.Option{ + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: true, + reason: "equal because EquateNaNs and EquateApprox compose together", + }, { + label: "EquateApprox+EquateNaNs", + x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001}, + y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002}, + opts: []cmp.Option{ + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: false, + reason: "not equal because EquateApprox and EquateNaNs do not apply on a named type", + }, { + label: "EquateApprox+EquateNaNs+Transform", + x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001}, + y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002}, + opts: []cmp.Option{ + cmp.Transformer("", func(x MyFloat) float64 { return float64(x) }), + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: true, + reason: "equal because named type is transformed to float64", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(0)}, + wantEqual: true, + reason: "equal because times are identical", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because time is exactly at the allowed margin", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because time is exactly at the allowed margin (negative)", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)}, + wantEqual: false, + reason: "not equal because time is outside allowed margin", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)}, + wantEqual: false, + reason: "not equal because time is outside allowed margin (negative)", + }, { + label: "EquateApproxTime", + x: time.Time{}, + y: time.Time{}, + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because both times are zero", + }, { + label: "EquateApproxTime", + x: time.Time{}, + y: time.Time{}.Add(1), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: false, + reason: "not equal because zero time is always not equal not non-zero", + }, { + label: "EquateApproxTime", + x: time.Time{}.Add(1), + y: time.Time{}, + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: false, + reason: "not equal because zero time is always not equal not non-zero", + }, { + label: "EquateApproxTime", + x: time.Date(2409, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2000, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: false, + reason: "time difference overflows time.Duration", + }, { + label: "EquateErrors", + x: nil, + y: nil, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: errors.New("EOF"), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: nil, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { + label: "EquateErrors", + x: nil, + y: nil, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: errors.New("EOF"), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: nil, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { + label: "EquateErrors", + x: struct{ E error }{nil}, + y: struct{ E error }{nil}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: struct{ E error }{errors.New("EOF")}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: struct{ E error }{xerrors.Errorf("wrapped: %w", io.EOF)}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: struct{ E error }{xerrors.Errorf("wrapped: %w", io.EOF)}, + y: struct{ E error }{io.EOF}, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{AnyError}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{AnyError}, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: struct{ E error }{nil}, + y: struct{ E error }{AnyError}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + wantEqual: false, + reason: "not equal because values do not match in deeply embedded field", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo1.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo1.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo2.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo2.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Foo2.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Foo2.Alpha", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + wantEqual: false, + reason: "not equal because many deeply nested or embedded fields differ", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Foo3", "Alpha")}, + wantEqual: true, + reason: "equal because IgnoreFields ignores fields at the highest levels", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{ + IgnoreFields(Bar3{}, + "Bar1.Foo3.Bravo", + "Bravo.Bar1.Foo3.Foo2.Foo1.Charlie", + "Bravo.Foo3.Foo2.Foo1.Bravo", + "Bravo.Bravo", + "Delta.Echo.Charlie", + "Foo3.Foo2.Foo1.Alpha", + "Alpha", + ), + }, + wantEqual: true, + reason: "equal because IgnoreFields ignores fields using fully-qualified field", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{ + IgnoreFields(Bar3{}, + "Bar1.Foo3.Bravo", + "Bravo.Foo3.Foo2.Foo1.Bravo", + "Bravo.Bravo", + "Delta.Echo.Charlie", + "Foo3.Foo2.Foo1.Alpha", + "Alpha", + ), + }, + wantEqual: false, + reason: "not equal because one fully-qualified field is not ignored: Bravo.Bar1.Foo3.Foo2.Foo1.Charlie", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha")}, + wantEqual: false, + reason: "not equal because highest-level field is not ignored: Foo3", + }, { + label: "IgnoreFields", + x: ParentStruct{ + privateStruct: &privateStruct{private: 1}, + PublicStruct: &PublicStruct{private: 2}, + private: 3, + }, + y: ParentStruct{ + privateStruct: &privateStruct{private: 10}, + PublicStruct: &PublicStruct{private: 20}, + private: 30, + }, + opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{})}, + wantEqual: false, + reason: "not equal because unexported fields mismatch", + }, { + label: "IgnoreFields", + x: ParentStruct{ + privateStruct: &privateStruct{private: 1}, + PublicStruct: &PublicStruct{private: 2}, + private: 3, + }, + y: ParentStruct{ + privateStruct: &privateStruct{private: 10}, + PublicStruct: &PublicStruct{private: 20}, + private: 30, + }, + opts: []cmp.Option{ + cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{}), + IgnoreFields(ParentStruct{}, "PublicStruct.private", "privateStruct.private", "private"), + }, + wantEqual: true, + reason: "equal because mismatching unexported fields are ignored", + }, { + label: "IgnoreTypes", + x: []interface{}{5, "same"}, + y: []interface{}{6, "same"}, + wantEqual: false, + reason: "not equal because 5 != 6", + }, { + label: "IgnoreTypes", + x: []interface{}{5, "same"}, + y: []interface{}{6, "same"}, + opts: []cmp.Option{IgnoreTypes(0)}, + wantEqual: true, + reason: "equal because ints are ignored", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "same", new(bytes.Buffer)}, + opts: []cmp.Option{IgnoreTypes(0)}, + wantPanic: true, + reason: "panics because bytes.Buffer has unexported fields", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "diff", new(bytes.Buffer)}, + opts: []cmp.Option{ + IgnoreTypes(0, ""), + IgnoreInterfaces(struct{ io.Reader }{}), + }, + wantEqual: true, + reason: "equal because bytes.Buffer is ignored by match on interface type", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "same", new(bytes.Buffer)}, + opts: []cmp.Option{ + IgnoreTypes(0, ""), + IgnoreInterfaces(struct { + io.Reader + io.Writer + fmt.Stringer + }{}), + }, + wantEqual: true, + reason: "equal because bytes.Buffer is ignored by match on multiple interface types", + }, { + label: "IgnoreInterfaces", + x: struct{ mu sync.Mutex }{}, + y: struct{ mu sync.Mutex }{}, + wantPanic: true, + reason: "panics because sync.Mutex has unexported fields", + }, { + label: "IgnoreInterfaces", + x: struct{ mu sync.Mutex }{}, + y: struct{ mu sync.Mutex }{}, + opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})}, + wantEqual: true, + reason: "equal because IgnoreInterfaces applies on values (with pointer receiver)", + }, { + label: "IgnoreInterfaces", + x: struct{ mu *sync.Mutex }{}, + y: struct{ mu *sync.Mutex }{}, + opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})}, + wantEqual: true, + reason: "equal because IgnoreInterfaces applies on pointers", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2}, + y: ParentStruct{Public: 1, private: -2}, + opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{})}, + wantEqual: false, + reason: "not equal because ParentStruct.private differs with AllowUnexported", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2}, + y: ParentStruct{Public: 1, private: -2}, + opts: []cmp.Option{IgnoreUnexported(ParentStruct{})}, + wantEqual: true, + reason: "equal because IgnoreUnexported ignored ParentStruct.private", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because ParentStruct.private is ignored", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: false, + reason: "not equal because ParentStruct.PublicStruct.private differs and not ignored by IgnoreUnexported(ParentStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + IgnoreUnexported(ParentStruct{}, PublicStruct{}), + }, + wantEqual: true, + reason: "equal because both ParentStruct.PublicStruct and ParentStruct.PublicStruct.private are ignored", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(privateStruct{}, PublicStruct{}, ParentStruct{}), + }, + wantEqual: false, + reason: "not equal since ParentStruct.privateStruct differs", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(privateStruct{}, PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because ParentStruct.privateStruct ignored by IgnoreUnexported(ParentStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}, ParentStruct{}), + IgnoreUnexported(privateStruct{}), + }, + wantEqual: true, + reason: "equal because privateStruct.private ignored by IgnoreUnexported(privateStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}, ParentStruct{}), + IgnoreUnexported(privateStruct{}), + }, + wantEqual: false, + reason: "not equal because privateStruct.Public differs and not ignored by IgnoreUnexported(privateStruct{})", + }, { + label: "IgnoreFields+IgnoreTypes+IgnoreUnexported", + x: &Everything{ + MyInt: 5, + MyFloat: 3.3, + MyTime: MyTime{time.Now()}, + Bar3: *createBar3X(), + ParentStruct: ParentStruct{ + Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}, + }, + }, + y: &Everything{ + MyInt: -5, + MyFloat: 3.3, + MyTime: MyTime{time.Now()}, + Bar3: *createBar3Y(), + ParentStruct: ParentStruct{ + Public: 1, private: -2, PublicStruct: &PublicStruct{Public: -3, private: -4}, + }, + }, + opts: []cmp.Option{ + IgnoreFields(Everything{}, "MyTime", "Bar3.Foo3"), + IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha"), + IgnoreTypes(MyInt(0), PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because all Ignore options can be composed together", + }, { + label: "IgnoreSliceElements", + x: []int{1, 0, 2, 3, 0, 4, 0, 0}, + y: []int{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: true, + reason: "equal because zero elements are ignored", + }, { + label: "IgnoreSliceElements", + x: []MyInt{1, 0, 2, 3, 0, 4, 0, 0}, + y: []MyInt{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: false, + reason: "not equal because MyInt is not assignable to int", + }, { + label: "IgnoreSliceElements", + x: MyInts{1, 0, 2, 3, 0, 4, 0, 0}, + y: MyInts{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: true, + reason: "equal because the element type of MyInts is assignable to int", + }, { + label: "IgnoreSliceElements+EquateEmpty", + x: []MyInt{}, + y: []MyInt{0, 0, 0, 0}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + EquateEmpty(), + }, + wantEqual: false, + reason: "not equal because ignored elements does not imply empty slice", + }, { + label: "IgnoreMapEntries", + x: map[string]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[string]int{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: true, + reason: "equal because uppercase keys are ignored", + }, { + label: "IgnoreMapEntries", + x: map[MyString]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[MyString]int{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: false, + reason: "not equal because MyString is not assignable to string", + }, { + label: "IgnoreMapEntries", + x: map[string]MyInt{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[string]MyInt{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: false, + reason: "not equal because MyInt is not assignable to int", + }, { + label: "IgnoreMapEntries+EquateEmpty", + x: map[string]MyInt{"ONE": 1, "TWO": 2, "THREE": 3}, + y: nil, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + EquateEmpty(), + }, + wantEqual: false, + reason: "not equal because ignored entries does not imply empty map", + }, { + label: "AcyclicTransformer", + x: "a\nb\nc\nd", + y: "a\nb\nd\nd", + opts: []cmp.Option{ + AcyclicTransformer("", func(s string) []string { return strings.Split(s, "\n") }), + }, + wantEqual: false, + reason: "not equal because 3rd line differs, but should not recurse infinitely", + }, { + label: "AcyclicTransformer", + x: []string{"foo", "Bar", "BAZ"}, + y: []string{"Foo", "BAR", "baz"}, + opts: []cmp.Option{ + AcyclicTransformer("", strings.ToUpper), + }, + wantEqual: true, + reason: "equal because of strings.ToUpper; AcyclicTransformer unnecessary, but check this still works", + }, { + label: "AcyclicTransformer", + x: "this is a sentence", + y: "this is a sentence", + opts: []cmp.Option{ + AcyclicTransformer("", strings.Fields), + }, + wantEqual: true, + reason: "equal because acyclic transformer splits on any contiguous whitespace", + }} + + for _, tt := range tests { + t.Run(tt.label, func(t *testing.T) { + var gotEqual bool + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + gotPanic = fmt.Sprint(ex) + } + }() + gotEqual = cmp.Equal(tt.x, tt.y, tt.opts...) + }() + switch { + case tt.reason == "": + t.Errorf("reason must be provided") + case gotPanic == "" && tt.wantPanic: + t.Errorf("expected Equal panic\nreason: %s", tt.reason) + case gotPanic != "" && !tt.wantPanic: + t.Errorf("unexpected Equal panic: got %v\nreason: %v", gotPanic, tt.reason) + case gotEqual != tt.wantEqual: + t.Errorf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason) + } + }) + } +} + +func TestPanic(t *testing.T) { + args := func(x ...interface{}) []interface{} { return x } + tests := []struct { + label string // Test name + fnc interface{} // Option function to call + args []interface{} // Arguments to pass in + wantPanic string // Expected panic message + reason string // The reason for the expected outcome + }{{ + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, 0.0), + reason: "zero margin and fraction is equivalent to exact equality", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(-0.1, 0.0), + wantPanic: "margin or fraction must be a non-negative number", + reason: "negative inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, -0.1), + wantPanic: "margin or fraction must be a non-negative number", + reason: "negative inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(math.NaN(), 0.0), + wantPanic: "margin or fraction must be a non-negative number", + reason: "NaN inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(1.0, 0.0), + reason: "fraction of 1.0 or greater is valid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, math.Inf(+1)), + reason: "margin of infinity is valid", + }, { + label: "EquateApproxTime", + fnc: EquateApproxTime, + args: args(time.Duration(-1)), + wantPanic: "margin must be a non-negative number", + reason: "negative duration is invalid", + }, { + label: "SortSlices", + fnc: SortSlices, + args: args(strings.Compare), + wantPanic: "invalid less function", + reason: "func(x, y string) int is wrong signature for less", + }, { + label: "SortSlices", + fnc: SortSlices, + args: args((func(_, _ int) bool)(nil)), + wantPanic: "invalid less function", + reason: "nil value is not valid", + }, { + label: "SortMaps", + fnc: SortMaps, + args: args(strings.Compare), + wantPanic: "invalid less function", + reason: "func(x, y string) int is wrong signature for less", + }, { + label: "SortMaps", + fnc: SortMaps, + args: args((func(_, _ int) bool)(nil)), + wantPanic: "invalid less function", + reason: "nil value is not valid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, ""), + wantPanic: "name must not be empty", + reason: "empty selector is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "."), + wantPanic: "name must not be empty", + reason: "single dot selector is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, ".Alpha"), + reason: "dot-prefix is okay since Foo1.Alpha reads naturally", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha."), + wantPanic: "name must not be empty", + reason: "dot-suffix is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha "), + wantPanic: "does not exist", + reason: "identifiers must not have spaces", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Zulu"), + wantPanic: "does not exist", + reason: "name of non-existent field is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha.NoExist"), + wantPanic: "must be a struct", + reason: "cannot select into a non-struct", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(&Foo1{}, "Alpha"), + wantPanic: "must be a non-pointer struct", + reason: "the type must be a struct (not pointer to a struct)", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(struct{ privateStruct }{}, "privateStruct"), + reason: "privateStruct field permitted since it is the default name of the embedded type", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(struct{ privateStruct }{}, "Public"), + reason: "Public field permitted since it is a forwarded field that is exported", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(struct{ privateStruct }{}, "private"), + wantPanic: "does not exist", + reason: "private field not permitted since it is a forwarded field that is unexported", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + reason: "empty input is valid", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + args: args(nil), + wantPanic: "cannot determine type", + reason: "input must not be nil value", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + args: args(0, 0, 0), + reason: "duplicate inputs of the same type is valid", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(nil), + wantPanic: "input must be an anonymous struct", + reason: "input must not be nil value", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(Foo1{}), + wantPanic: "input must be an anonymous struct", + reason: "input must not be a named struct type", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ _ io.Reader }{}), + wantPanic: "struct cannot have named fields", + reason: "input must not have named fields", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ Foo1 }{}), + wantPanic: "embedded field must be an interface type", + reason: "field types must be interfaces", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ EmptyInterface }{}), + wantPanic: "cannot ignore empty interface", + reason: "field types must not be the empty interface", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct { + io.Reader + io.Writer + io.Closer + io.ReadWriteCloser + }{}), + reason: "multiple interfaces may be specified, even if they overlap", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + reason: "empty input is valid", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(nil), + wantPanic: "must be a non-pointer struct", + reason: "input must not be nil value", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(&Foo1{}), + wantPanic: "must be a non-pointer struct", + reason: "input must be a struct type (not a pointer to a struct)", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(Foo1{}, struct{ x, X int }{}), + reason: "input may be named or unnamed structs", + }, { + label: "AcyclicTransformer", + fnc: AcyclicTransformer, + args: args("", "not a func"), + wantPanic: "invalid transformer function", + reason: "AcyclicTransformer has same input requirements as Transformer", + }} + + for _, tt := range tests { + t.Run(tt.label, func(t *testing.T) { + // Prepare function arguments. + vf := reflect.ValueOf(tt.fnc) + var vargs []reflect.Value + for i, arg := range tt.args { + if arg == nil { + tf := vf.Type() + if i == tf.NumIn()-1 && tf.IsVariadic() { + vargs = append(vargs, reflect.Zero(tf.In(i).Elem())) + } else { + vargs = append(vargs, reflect.Zero(tf.In(i))) + } + } else { + vargs = append(vargs, reflect.ValueOf(arg)) + } + } + + // Call the function and capture any panics. + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + vf.Call(vargs) + }() + + switch { + case tt.reason == "": + t.Errorf("reason must be provided") + case tt.wantPanic == "" && gotPanic != "": + t.Errorf("unexpected panic message: %s\nreason: %s", gotPanic, tt.reason) + case tt.wantPanic != "" && !strings.Contains(gotPanic, tt.wantPanic): + t.Errorf("panic message:\ngot: %s\nwant: %s\nreason: %s", gotPanic, tt.wantPanic, tt.reason) + } + }) + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/xform.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/xform.go new file mode 100644 index 0000000..4eb49d6 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/cmpopts/xform.go @@ -0,0 +1,35 @@ +// Copyright 2018, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts + +import ( + "github.com/google/go-cmp/cmp" +) + +type xformFilter struct{ xform cmp.Option } + +func (xf xformFilter) filter(p cmp.Path) bool { + for _, ps := range p { + if t, ok := ps.(cmp.Transform); ok && t.Option() == xf.xform { + return false + } + } + return true +} + +// AcyclicTransformer returns a Transformer with a filter applied that ensures +// that the transformer cannot be recursively applied upon its own output. +// +// An example use case is a transformer that splits a string by lines: +// AcyclicTransformer("SplitLines", func(s string) []string{ +// return strings.Split(s, "\n") +// }) +// +// Had this been an unfiltered Transformer instead, this would result in an +// infinite cycle converting a string to []string to [][]string and so on. +func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option { + xf := xformFilter{cmp.Transformer(name, xformFunc)} + return cmp.FilterPath(xf.filter, xf.xform) +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/compare.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/compare.go new file mode 100644 index 0000000..86d0903 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/compare.go @@ -0,0 +1,682 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cmp determines equality of values. +// +// This package is intended to be a more powerful and safer alternative to +// reflect.DeepEqual for comparing whether two values are semantically equal. +// It is intended to only be used in tests, as performance is not a goal and +// it may panic if it cannot compare the values. Its propensity towards +// panicking means that its unsuitable for production environments where a +// spurious panic may be fatal. +// +// The primary features of cmp are: +// +// • When the default behavior of equality does not suit the needs of the test, +// custom equality functions can override the equality operation. +// For example, an equality function may report floats as equal so long as they +// are within some tolerance of each other. +// +// • Types that have an Equal method may use that method to determine equality. +// This allows package authors to determine the equality operation for the types +// that they define. +// +// • If no custom equality functions are used and no Equal method is defined, +// equality is determined by recursively comparing the primitive kinds on both +// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported +// fields are not compared by default; they result in panics unless suppressed +// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly +// compared using the Exporter option. +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/diff" + "github.com/google/go-cmp/cmp/internal/flags" + "github.com/google/go-cmp/cmp/internal/function" + "github.com/google/go-cmp/cmp/internal/value" +) + +// Equal reports whether x and y are equal by recursively applying the +// following rules in the given order to x and y and all of their sub-values: +// +// • Let S be the set of all Ignore, Transformer, and Comparer options that +// remain after applying all path filters, value filters, and type filters. +// If at least one Ignore exists in S, then the comparison is ignored. +// If the number of Transformer and Comparer options in S is greater than one, +// then Equal panics because it is ambiguous which option to use. +// If S contains a single Transformer, then use that to transform the current +// values and recursively call Equal on the output values. +// If S contains a single Comparer, then use that to compare the current values. +// Otherwise, evaluation proceeds to the next rule. +// +// • If the values have an Equal method of the form "(T) Equal(T) bool" or +// "(T) Equal(I) bool" where T is assignable to I, then use the result of +// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and +// evaluation proceeds to the next rule. +// +// • Lastly, try to compare x and y based on their basic kinds. +// Simple kinds like booleans, integers, floats, complex numbers, strings, and +// channels are compared using the equivalent of the == operator in Go. +// Functions are only equal if they are both nil, otherwise they are unequal. +// +// Structs are equal if recursively calling Equal on all fields report equal. +// If a struct contains unexported fields, Equal panics unless an Ignore option +// (e.g., cmpopts.IgnoreUnexported) ignores that field or the Exporter option +// explicitly permits comparing the unexported field. +// +// Slices are equal if they are both nil or both non-nil, where recursively +// calling Equal on all non-ignored slice or array elements report equal. +// Empty non-nil slices and nil slices are not equal; to equate empty slices, +// consider using cmpopts.EquateEmpty. +// +// Maps are equal if they are both nil or both non-nil, where recursively +// calling Equal on all non-ignored map entries report equal. +// Map keys are equal according to the == operator. +// To use custom comparisons for map keys, consider using cmpopts.SortMaps. +// Empty non-nil maps and nil maps are not equal; to equate empty maps, +// consider using cmpopts.EquateEmpty. +// +// Pointers and interfaces are equal if they are both nil or both non-nil, +// where they have the same underlying concrete type and recursively +// calling Equal on the underlying values reports equal. +// +// Before recursing into a pointer, slice element, or map, the current path +// is checked to detect whether the address has already been visited. +// If there is a cycle, then the pointed at values are considered equal +// only if both addresses were previously visited in the same path step. +func Equal(x, y interface{}, opts ...Option) bool { + s := newState(opts) + s.compareAny(rootStep(x, y)) + return s.result.Equal() +} + +// Diff returns a human-readable report of the differences between two values: +// y - x. It returns an empty string if and only if Equal returns true for the +// same input values and options. +// +// The output is displayed as a literal in pseudo-Go syntax. +// At the start of each line, a "-" prefix indicates an element removed from x, +// a "+" prefix to indicates an element added from y, and the lack of a prefix +// indicates an element common to both x and y. If possible, the output +// uses fmt.Stringer.String or error.Error methods to produce more humanly +// readable outputs. In such cases, the string is prefixed with either an +// 's' or 'e' character, respectively, to indicate that the method was called. +// +// Do not depend on this output being stable. If you need the ability to +// programmatically interpret the difference, consider using a custom Reporter. +func Diff(x, y interface{}, opts ...Option) string { + s := newState(opts) + + // Optimization: If there are no other reporters, we can optimize for the + // common case where the result is equal (and thus no reported difference). + // This avoids the expensive construction of a difference tree. + if len(s.reporters) == 0 { + s.compareAny(rootStep(x, y)) + if s.result.Equal() { + return "" + } + s.result = diff.Result{} // Reset results + } + + r := new(defaultReporter) + s.reporters = append(s.reporters, reporter{r}) + s.compareAny(rootStep(x, y)) + d := r.String() + if (d == "") != s.result.Equal() { + panic("inconsistent difference and equality results") + } + return d +} + +// rootStep constructs the first path step. If x and y have differing types, +// then they are stored within an empty interface type. +func rootStep(x, y interface{}) PathStep { + vx := reflect.ValueOf(x) + vy := reflect.ValueOf(y) + + // If the inputs are different types, auto-wrap them in an empty interface + // so that they have the same parent type. + var t reflect.Type + if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() { + t = reflect.TypeOf((*interface{})(nil)).Elem() + if vx.IsValid() { + vvx := reflect.New(t).Elem() + vvx.Set(vx) + vx = vvx + } + if vy.IsValid() { + vvy := reflect.New(t).Elem() + vvy.Set(vy) + vy = vvy + } + } else { + t = vx.Type() + } + + return &pathStep{t, vx, vy} +} + +type state struct { + // These fields represent the "comparison state". + // Calling statelessCompare must not result in observable changes to these. + result diff.Result // The current result of comparison + curPath Path // The current path in the value tree + curPtrs pointerPath // The current set of visited pointers + reporters []reporter // Optional reporters + + // recChecker checks for infinite cycles applying the same set of + // transformers upon the output of itself. + recChecker recChecker + + // dynChecker triggers pseudo-random checks for option correctness. + // It is safe for statelessCompare to mutate this value. + dynChecker dynChecker + + // These fields, once set by processOption, will not change. + exporters []exporter // List of exporters for structs with unexported fields + opts Options // List of all fundamental and filter options +} + +func newState(opts []Option) *state { + // Always ensure a validator option exists to validate the inputs. + s := &state{opts: Options{validator{}}} + s.curPtrs.Init() + s.processOption(Options(opts)) + return s +} + +func (s *state) processOption(opt Option) { + switch opt := opt.(type) { + case nil: + case Options: + for _, o := range opt { + s.processOption(o) + } + case coreOption: + type filtered interface { + isFiltered() bool + } + if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() { + panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) + } + s.opts = append(s.opts, opt) + case exporter: + s.exporters = append(s.exporters, opt) + case reporter: + s.reporters = append(s.reporters, opt) + default: + panic(fmt.Sprintf("unknown option %T", opt)) + } +} + +// statelessCompare compares two values and returns the result. +// This function is stateless in that it does not alter the current result, +// or output to any registered reporters. +func (s *state) statelessCompare(step PathStep) diff.Result { + // We do not save and restore curPath and curPtrs because all of the + // compareX methods should properly push and pop from them. + // It is an implementation bug if the contents of the paths differ from + // when calling this function to when returning from it. + + oldResult, oldReporters := s.result, s.reporters + s.result = diff.Result{} // Reset result + s.reporters = nil // Remove reporters to avoid spurious printouts + s.compareAny(step) + res := s.result + s.result, s.reporters = oldResult, oldReporters + return res +} + +func (s *state) compareAny(step PathStep) { + // Update the path stack. + s.curPath.push(step) + defer s.curPath.pop() + for _, r := range s.reporters { + r.PushStep(step) + defer r.PopStep() + } + s.recChecker.Check(s.curPath) + + // Cycle-detection for slice elements (see NOTE in compareSlice). + t := step.Type() + vx, vy := step.Values() + if si, ok := step.(SliceIndex); ok && si.isSlice && vx.IsValid() && vy.IsValid() { + px, py := vx.Addr(), vy.Addr() + if eq, visited := s.curPtrs.Push(px, py); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(px, py) + } + + // Rule 1: Check whether an option applies on this node in the value tree. + if s.tryOptions(t, vx, vy) { + return + } + + // Rule 2: Check whether the type has a valid Equal method. + if s.tryMethod(t, vx, vy) { + return + } + + // Rule 3: Compare based on the underlying kind. + switch t.Kind() { + case reflect.Bool: + s.report(vx.Bool() == vy.Bool(), 0) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s.report(vx.Int() == vy.Int(), 0) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s.report(vx.Uint() == vy.Uint(), 0) + case reflect.Float32, reflect.Float64: + s.report(vx.Float() == vy.Float(), 0) + case reflect.Complex64, reflect.Complex128: + s.report(vx.Complex() == vy.Complex(), 0) + case reflect.String: + s.report(vx.String() == vy.String(), 0) + case reflect.Chan, reflect.UnsafePointer: + s.report(vx.Pointer() == vy.Pointer(), 0) + case reflect.Func: + s.report(vx.IsNil() && vy.IsNil(), 0) + case reflect.Struct: + s.compareStruct(t, vx, vy) + case reflect.Slice, reflect.Array: + s.compareSlice(t, vx, vy) + case reflect.Map: + s.compareMap(t, vx, vy) + case reflect.Ptr: + s.comparePtr(t, vx, vy) + case reflect.Interface: + s.compareInterface(t, vx, vy) + default: + panic(fmt.Sprintf("%v kind not handled", t.Kind())) + } +} + +func (s *state) tryOptions(t reflect.Type, vx, vy reflect.Value) bool { + // Evaluate all filters and apply the remaining options. + if opt := s.opts.filter(s, t, vx, vy); opt != nil { + opt.apply(s, vx, vy) + return true + } + return false +} + +func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool { + // Check if this type even has an Equal method. + m, ok := t.MethodByName("Equal") + if !ok || !function.IsType(m.Type, function.EqualAssignable) { + return false + } + + eq := s.callTTBFunc(m.Func, vx, vy) + s.report(eq, reportByMethod) + return true +} + +func (s *state) callTRFunc(f, v reflect.Value, step Transform) reflect.Value { + v = sanitizeValue(v, f.Type().In(0)) + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{v})[0] + } + + // Run the function twice and ensure that we get the same results back. + // We run in goroutines so that the race detector (if enabled) can detect + // unsafe mutations to the input. + c := make(chan reflect.Value) + go detectRaces(c, f, v) + got := <-c + want := f.Call([]reflect.Value{v})[0] + if step.vx, step.vy = got, want; !s.statelessCompare(step).Equal() { + // To avoid false-positives with non-reflexive equality operations, + // we sanity check whether a value is equal to itself. + if step.vx, step.vy = want, want; !s.statelessCompare(step).Equal() { + return want + } + panic(fmt.Sprintf("non-deterministic function detected: %s", function.NameOf(f))) + } + return want +} + +func (s *state) callTTBFunc(f, x, y reflect.Value) bool { + x = sanitizeValue(x, f.Type().In(0)) + y = sanitizeValue(y, f.Type().In(1)) + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{x, y})[0].Bool() + } + + // Swapping the input arguments is sufficient to check that + // f is symmetric and deterministic. + // We run in goroutines so that the race detector (if enabled) can detect + // unsafe mutations to the input. + c := make(chan reflect.Value) + go detectRaces(c, f, y, x) + got := <-c + want := f.Call([]reflect.Value{x, y})[0].Bool() + if !got.IsValid() || got.Bool() != want { + panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", function.NameOf(f))) + } + return want +} + +func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { + var ret reflect.Value + defer func() { + recover() // Ignore panics, let the other call to f panic instead + c <- ret + }() + ret = f.Call(vs)[0] +} + +// sanitizeValue converts nil interfaces of type T to those of type R, +// assuming that T is assignable to R. +// Otherwise, it returns the input value as is. +func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { + // TODO(≥go1.10): Workaround for reflect bug (https://golang.org/issue/22143). + if !flags.AtLeastGo110 { + if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t { + return reflect.New(t).Elem() + } + } + return v +} + +func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { + var addr bool + var vax, vay reflect.Value // Addressable versions of vx and vy + + var mayForce, mayForceInit bool + step := StructField{&structField{}} + for i := 0; i < t.NumField(); i++ { + step.typ = t.Field(i).Type + step.vx = vx.Field(i) + step.vy = vy.Field(i) + step.name = t.Field(i).Name + step.idx = i + step.unexported = !isExported(step.name) + if step.unexported { + if step.name == "_" { + continue + } + // Defer checking of unexported fields until later to give an + // Ignore a chance to ignore the field. + if !vax.IsValid() || !vay.IsValid() { + // For retrieveUnexportedField to work, the parent struct must + // be addressable. Create a new copy of the values if + // necessary to make them addressable. + addr = vx.CanAddr() || vy.CanAddr() + vax = makeAddressable(vx) + vay = makeAddressable(vy) + } + if !mayForceInit { + for _, xf := range s.exporters { + mayForce = mayForce || xf(t) + } + mayForceInit = true + } + step.mayForce = mayForce + step.paddr = addr + step.pvx = vax + step.pvy = vay + step.field = t.Field(i) + } + s.compareAny(step) + } +} + +func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { + isSlice := t.Kind() == reflect.Slice + if isSlice && (vx.IsNil() || vy.IsNil()) { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + + // NOTE: It is incorrect to call curPtrs.Push on the slice header pointer + // since slices represents a list of pointers, rather than a single pointer. + // The pointer checking logic must be handled on a per-element basis + // in compareAny. + // + // A slice header (see reflect.SliceHeader) in Go is a tuple of a starting + // pointer P, a length N, and a capacity C. Supposing each slice element has + // a memory size of M, then the slice is equivalent to the list of pointers: + // [P+i*M for i in range(N)] + // + // For example, v[:0] and v[:1] are slices with the same starting pointer, + // but they are clearly different values. Using the slice pointer alone + // violates the assumption that equal pointers implies equal values. + + step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}, isSlice: isSlice}} + withIndexes := func(ix, iy int) SliceIndex { + if ix >= 0 { + step.vx, step.xkey = vx.Index(ix), ix + } else { + step.vx, step.xkey = reflect.Value{}, -1 + } + if iy >= 0 { + step.vy, step.ykey = vy.Index(iy), iy + } else { + step.vy, step.ykey = reflect.Value{}, -1 + } + return step + } + + // Ignore options are able to ignore missing elements in a slice. + // However, detecting these reliably requires an optimal differencing + // algorithm, for which diff.Difference is not. + // + // Instead, we first iterate through both slices to detect which elements + // would be ignored if standing alone. The index of non-discarded elements + // are stored in a separate slice, which diffing is then performed on. + var indexesX, indexesY []int + var ignoredX, ignoredY []bool + for ix := 0; ix < vx.Len(); ix++ { + ignored := s.statelessCompare(withIndexes(ix, -1)).NumDiff == 0 + if !ignored { + indexesX = append(indexesX, ix) + } + ignoredX = append(ignoredX, ignored) + } + for iy := 0; iy < vy.Len(); iy++ { + ignored := s.statelessCompare(withIndexes(-1, iy)).NumDiff == 0 + if !ignored { + indexesY = append(indexesY, iy) + } + ignoredY = append(ignoredY, ignored) + } + + // Compute an edit-script for slices vx and vy (excluding ignored elements). + edits := diff.Difference(len(indexesX), len(indexesY), func(ix, iy int) diff.Result { + return s.statelessCompare(withIndexes(indexesX[ix], indexesY[iy])) + }) + + // Replay the ignore-scripts and the edit-script. + var ix, iy int + for ix < vx.Len() || iy < vy.Len() { + var e diff.EditType + switch { + case ix < len(ignoredX) && ignoredX[ix]: + e = diff.UniqueX + case iy < len(ignoredY) && ignoredY[iy]: + e = diff.UniqueY + default: + e, edits = edits[0], edits[1:] + } + switch e { + case diff.UniqueX: + s.compareAny(withIndexes(ix, -1)) + ix++ + case diff.UniqueY: + s.compareAny(withIndexes(-1, iy)) + iy++ + default: + s.compareAny(withIndexes(ix, iy)) + ix++ + iy++ + } + } +} + +func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + + // Cycle-detection for maps. + if eq, visited := s.curPtrs.Push(vx, vy); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(vx, vy) + + // We combine and sort the two map keys so that we can perform the + // comparisons in a deterministic order. + step := MapIndex{&mapIndex{pathStep: pathStep{typ: t.Elem()}}} + for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { + step.vx = vx.MapIndex(k) + step.vy = vy.MapIndex(k) + step.key = k + if !step.vx.IsValid() && !step.vy.IsValid() { + // It is possible for both vx and vy to be invalid if the + // key contained a NaN value in it. + // + // Even with the ability to retrieve NaN keys in Go 1.12, + // there still isn't a sensible way to compare the values since + // a NaN key may map to multiple unordered values. + // The most reasonable way to compare NaNs would be to compare the + // set of values. However, this is impossible to do efficiently + // since set equality is provably an O(n^2) operation given only + // an Equal function. If we had a Less function or Hash function, + // this could be done in O(n*log(n)) or O(n), respectively. + // + // Rather than adding complex logic to deal with NaNs, make it + // the user's responsibility to compare such obscure maps. + const help = "consider providing a Comparer to compare the map" + panic(fmt.Sprintf("%#v has map key with NaNs\n%s", s.curPath, help)) + } + s.compareAny(step) + } +} + +func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + + // Cycle-detection for pointers. + if eq, visited := s.curPtrs.Push(vx, vy); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(vx, vy) + + vx, vy = vx.Elem(), vy.Elem() + s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}}) +} + +func (s *state) compareInterface(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + vx, vy = vx.Elem(), vy.Elem() + if vx.Type() != vy.Type() { + s.report(false, 0) + return + } + s.compareAny(TypeAssertion{&typeAssertion{pathStep{vx.Type(), vx, vy}}}) +} + +func (s *state) report(eq bool, rf resultFlags) { + if rf&reportByIgnore == 0 { + if eq { + s.result.NumSame++ + rf |= reportEqual + } else { + s.result.NumDiff++ + rf |= reportUnequal + } + } + for _, r := range s.reporters { + r.Report(Result{flags: rf}) + } +} + +// recChecker tracks the state needed to periodically perform checks that +// user provided transformers are not stuck in an infinitely recursive cycle. +type recChecker struct{ next int } + +// Check scans the Path for any recursive transformers and panics when any +// recursive transformers are detected. Note that the presence of a +// recursive Transformer does not necessarily imply an infinite cycle. +// As such, this check only activates after some minimal number of path steps. +func (rc *recChecker) Check(p Path) { + const minLen = 1 << 16 + if rc.next == 0 { + rc.next = minLen + } + if len(p) < rc.next { + return + } + rc.next <<= 1 + + // Check whether the same transformer has appeared at least twice. + var ss []string + m := map[Option]int{} + for _, ps := range p { + if t, ok := ps.(Transform); ok { + t := t.Option() + if m[t] == 1 { // Transformer was used exactly once before + tf := t.(*transformer).fnc.Type() + ss = append(ss, fmt.Sprintf("%v: %v => %v", t, tf.In(0), tf.Out(0))) + } + m[t]++ + } + } + if len(ss) > 0 { + const warning = "recursive set of Transformers detected" + const help = "consider using cmpopts.AcyclicTransformer" + set := strings.Join(ss, "\n\t") + panic(fmt.Sprintf("%s:\n\t%s\n%s", warning, set, help)) + } +} + +// dynChecker tracks the state needed to periodically perform checks that +// user provided functions are symmetric and deterministic. +// The zero value is safe for immediate use. +type dynChecker struct{ curr, next int } + +// Next increments the state and reports whether a check should be performed. +// +// Checks occur every Nth function call, where N is a triangular number: +// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... +// See https://en.wikipedia.org/wiki/Triangular_number +// +// This sequence ensures that the cost of checks drops significantly as +// the number of functions calls grows larger. +func (dc *dynChecker) Next() bool { + ok := dc.curr == dc.next + if ok { + dc.curr = 0 + dc.next++ + } + dc.curr++ + return ok +} + +// makeAddressable returns a value that is always addressable. +// It returns the input verbatim if it is already addressable, +// otherwise it creates a new value and returns an addressable copy. +func makeAddressable(v reflect.Value) reflect.Value { + if v.CanAddr() { + return v + } + vc := reflect.New(v.Type()).Elem() + vc.Set(v) + return vc +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/compare_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/compare_test.go new file mode 100644 index 0000000..f7b1f13 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/compare_test.go @@ -0,0 +1,2900 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp_test + +import ( + "bytes" + "crypto/md5" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/go-cmp/cmp/internal/flags" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" + ts "github.com/google/go-cmp/cmp/internal/teststructs" + foo1 "github.com/google/go-cmp/cmp/internal/teststructs/foo1" + foo2 "github.com/google/go-cmp/cmp/internal/teststructs/foo2" +) + +func init() { + flags.Deterministic = true +} + +var update = flag.Bool("update", false, "update golden test files") + +const goldenHeaderPrefix = "<<< " +const goldenFooterPrefix = ">>> " + +/// mustParseGolden parses a file as a set of key-value pairs. +// +// The syntax is simple and looks something like: +// +// <<< Key1 +// value1a +// value1b +// >>> Key1 +// <<< Key2 +// value2 +// >>> Key2 +// +// It is the user's responsibility to choose a sufficiently unique key name +// such that it never appears in the body of the value itself. +func mustParseGolden(path string) map[string]string { + b, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + s := string(b) + + out := map[string]string{} + for len(s) > 0 { + // Identify the next header. + i := strings.Index(s, "\n") + len("\n") + header := s[:i] + if !strings.HasPrefix(header, goldenHeaderPrefix) { + panic(fmt.Sprintf("invalid header: %q", header)) + } + + // Locate the next footer. + footer := goldenFooterPrefix + header[len(goldenHeaderPrefix):] + j := strings.Index(s, footer) + if j < 0 { + panic(fmt.Sprintf("missing footer: %q", footer)) + } + + // Store the name and data. + name := header[len(goldenHeaderPrefix) : len(header)-len("\n")] + if _, ok := out[name]; ok { + panic(fmt.Sprintf("duplicate name: %q", name)) + } + out[name] = s[len(header):j] + s = s[j+len(footer):] + } + return out +} +func mustFormatGolden(path string, in []struct{ Name, Data string }) { + var b []byte + for _, v := range in { + b = append(b, goldenHeaderPrefix+v.Name+"\n"...) + b = append(b, v.Data...) + b = append(b, goldenFooterPrefix+v.Name+"\n"...) + } + if err := ioutil.WriteFile(path, b, 0664); err != nil { + panic(err) + } +} + +var now = time.Date(2009, time.November, 10, 23, 00, 00, 00, time.UTC) + +func newInt(n int) *int { return &n } + +type Stringer string + +func newStringer(s string) fmt.Stringer { return (*Stringer)(&s) } +func (s Stringer) String() string { return string(s) } + +type test struct { + label string // Test name + x, y interface{} // Input values to compare + opts []cmp.Option // Input options + wantEqual bool // Whether any difference is expected + wantPanic string // Sub-string of an expected panic message + reason string // The reason for the expected outcome +} + +func TestDiff(t *testing.T) { + var tests []test + tests = append(tests, comparerTests()...) + tests = append(tests, transformerTests()...) + tests = append(tests, reporterTests()...) + tests = append(tests, embeddedTests()...) + tests = append(tests, methodTests()...) + tests = append(tests, cycleTests()...) + tests = append(tests, project1Tests()...) + tests = append(tests, project2Tests()...) + tests = append(tests, project3Tests()...) + tests = append(tests, project4Tests()...) + + const goldenFile = "testdata/diffs" + gotDiffs := []struct{ Name, Data string }{} + wantDiffs := mustParseGolden(goldenFile) + for _, tt := range tests { + tt := tt + t.Run(tt.label, func(t *testing.T) { + if !*update { + t.Parallel() + } + var gotDiff, gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + gotDiff = cmp.Diff(tt.x, tt.y, tt.opts...) + }() + + switch { + case strings.Contains(t.Name(), "#"): + panic("unique test name must be provided") + case tt.reason == "": + panic("reason must be provided") + case tt.wantPanic == "": + if gotPanic != "" { + t.Fatalf("unexpected panic message: %s\nreason: %v", gotPanic, tt.reason) + } + if *update { + if gotDiff != "" { + gotDiffs = append(gotDiffs, struct{ Name, Data string }{t.Name(), gotDiff}) + } + } else { + wantDiff := wantDiffs[t.Name()] + if diff := cmp.Diff(wantDiff, gotDiff); diff != "" { + t.Fatalf("Diff:\ngot:\n%s\nwant:\n%s\ndiff: (-want +got)\n%s\nreason: %v", gotDiff, wantDiff, diff, tt.reason) + } + } + gotEqual := gotDiff == "" + if gotEqual != tt.wantEqual { + t.Fatalf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason) + } + default: + if !strings.Contains(gotPanic, tt.wantPanic) { + t.Fatalf("panic message:\ngot: %s\nwant: %s\nreason: %v", gotPanic, tt.wantPanic, tt.reason) + } + } + }) + } + + if *update { + mustFormatGolden(goldenFile, gotDiffs) + } +} + +func comparerTests() []test { + const label = "Comparer" + + type Iface1 interface { + Method() + } + type Iface2 interface { + Method() + } + + type tarHeader struct { + Name string + Mode int64 + Uid int + Gid int + Size int64 + ModTime time.Time + Typeflag byte + Linkname string + Uname string + Gname string + Devmajor int64 + Devminor int64 + AccessTime time.Time + ChangeTime time.Time + Xattrs map[string]string + } + + type namedWithUnexported struct { + unexported string + } + + makeTarHeaders := func(tf byte) (hs []tarHeader) { + for i := 0; i < 5; i++ { + hs = append(hs, tarHeader{ + Name: fmt.Sprintf("some/dummy/test/file%d", i), + Mode: 0664, Uid: i * 1000, Gid: i * 1000, Size: 1 << uint(i), + ModTime: now.Add(time.Duration(i) * time.Hour), + Uname: "user", Gname: "group", + Typeflag: tf, + }) + } + return hs + } + + return []test{{ + label: label + "/Nil", + x: nil, + y: nil, + wantEqual: true, + reason: "nils are equal", + }, { + label: label + "/Integer", + x: 1, + y: 1, + wantEqual: true, + reason: "identical integers are equal", + }, { + label: label + "/UnfilteredIgnore", + x: 1, + y: 1, + opts: []cmp.Option{cmp.Ignore()}, + wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", + }, { + label: label + "/UnfilteredCompare", + x: 1, + y: 1, + opts: []cmp.Option{cmp.Comparer(func(_, _ interface{}) bool { return true })}, + wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", + }, { + label: label + "/UnfilteredTransform", + x: 1, + y: 1, + opts: []cmp.Option{cmp.Transformer("λ", func(x interface{}) interface{} { return x })}, + wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", + }, { + label: label + "/AmbiguousOptions", + x: 1, + y: 1, + opts: []cmp.Option{ + cmp.Comparer(func(x, y int) bool { return true }), + cmp.Transformer("λ", func(x int) float64 { return float64(x) }), + }, + wantPanic: "ambiguous set of applicable options", + reason: "both options apply on int, leading to ambiguity", + }, { + label: label + "/IgnorePrecedence", + x: 1, + y: 1, + opts: []cmp.Option{ + cmp.FilterPath(func(p cmp.Path) bool { + return len(p) > 0 && p[len(p)-1].Type().Kind() == reflect.Int + }, cmp.Options{cmp.Ignore(), cmp.Ignore(), cmp.Ignore()}), + cmp.Comparer(func(x, y int) bool { return true }), + cmp.Transformer("λ", func(x int) float64 { return float64(x) }), + }, + wantEqual: true, + reason: "ignore takes precedence over other options", + }, { + label: label + "/UnknownOption", + opts: []cmp.Option{struct{ cmp.Option }{}}, + wantPanic: "unknown option", + reason: "use of unknown option should panic", + }, { + label: label + "/StructEqual", + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 3}, + wantEqual: true, + reason: "struct comparison with all equal fields", + }, { + label: label + "/StructInequal", + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 4}, + wantEqual: false, + reason: "struct comparison with inequal C field", + }, { + label: label + "/StructUnexported", + x: struct{ a, b, c int }{1, 2, 3}, + y: struct{ a, b, c int }{1, 2, 4}, + wantPanic: "cannot handle unexported field", + reason: "unexported fields result in a panic by default", + }, { + label: label + "/PointerStructEqual", + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(4)}, + wantEqual: true, + reason: "comparison of pointer to struct with equal A field", + }, { + label: label + "/PointerStructInequal", + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, + wantEqual: false, + reason: "comparison of pointer to struct with inequal A field", + }, { + label: label + "/PointerStructTrueComparer", + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y int) bool { return true }), + }, + wantEqual: true, + reason: "comparison of pointer to struct with inequal A field, but treated as equal with always equal comparer", + }, { + label: label + "/PointerStructNonNilComparer", + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x != nil && y != nil }), + }, + wantEqual: true, + reason: "comparison of pointer to struct with inequal A field, but treated as equal with comparer checking pointers for nilness", + }, { + label: label + "/StructNestedPointerEqual", + x: &struct{ R *bytes.Buffer }{}, + y: &struct{ R *bytes.Buffer }{}, + wantEqual: true, + reason: "equal since both pointers in R field are nil", + }, { + label: label + "/StructNestedPointerInequal", + x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, + y: &struct{ R *bytes.Buffer }{}, + wantEqual: false, + reason: "inequal since R field is inequal", + }, { + label: label + "/StructNestedPointerTrueComparer", + x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, + y: &struct{ R *bytes.Buffer }{}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y io.Reader) bool { return true }), + }, + wantEqual: true, + reason: "equal despite inequal R field values since the comparer always reports true", + }, { + label: label + "/StructNestedValueUnexportedPanic1", + x: &struct{ R bytes.Buffer }{}, + y: &struct{ R bytes.Buffer }{}, + wantPanic: "cannot handle unexported field", + reason: "bytes.Buffer contains unexported fields", + }, { + label: label + "/StructNestedValueUnexportedPanic2", + x: &struct{ R bytes.Buffer }{}, + y: &struct{ R bytes.Buffer }{}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y io.Reader) bool { return true }), + }, + wantPanic: "cannot handle unexported field", + reason: "bytes.Buffer value does not implement io.Reader", + }, { + label: label + "/StructNestedValueEqual", + x: &struct{ R bytes.Buffer }{}, + y: &struct{ R bytes.Buffer }{}, + opts: []cmp.Option{ + cmp.Transformer("Ref", func(x bytes.Buffer) *bytes.Buffer { return &x }), + cmp.Comparer(func(x, y io.Reader) bool { return true }), + }, + wantEqual: true, + reason: "bytes.Buffer pointer due to shallow copy does implement io.Reader", + }, { + label: label + "/RegexpUnexportedPanic", + x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + wantPanic: "cannot handle unexported field", + reason: "regexp.Regexp contains unexported fields", + }, { + label: label + "/RegexpEqual", + x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + opts: []cmp.Option{cmp.Comparer(func(x, y *regexp.Regexp) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() + })}, + wantEqual: true, + reason: "comparer for *regexp.Regexp applied with equal regexp strings", + }, { + label: label + "/RegexpInequal", + x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*d*")}, + opts: []cmp.Option{cmp.Comparer(func(x, y *regexp.Regexp) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() + })}, + wantEqual: false, + reason: "comparer for *regexp.Regexp applied with inequal regexp strings", + }, { + label: label + "/TriplePointerEqual", + x: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + y: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + wantEqual: true, + reason: "three layers of pointers to the same value", + }, { + label: label + "/TriplePointerInequal", + x: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + y: func() ***int { + a := 1 + b := &a + c := &b + return &c + }(), + wantEqual: false, + reason: "three layers of pointers to different values", + }, { + label: label + "/SliceWithDifferingCapacity", + x: []int{1, 2, 3, 4, 5}[:3], + y: []int{1, 2, 3}, + wantEqual: true, + reason: "elements past the slice length are not compared", + }, { + label: label + "/StringerEqual", + x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, + y: struct{ fmt.Stringer }{regexp.MustCompile("hello")}, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + wantEqual: true, + reason: "comparer for fmt.Stringer used to compare differing types with same string", + }, { + label: label + "/StringerInequal", + x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, + y: struct{ fmt.Stringer }{regexp.MustCompile("hello2")}, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + wantEqual: false, + reason: "comparer for fmt.Stringer used to compare differing types with different strings", + }, { + label: label + "/DifferingHash", + x: md5.Sum([]byte{'a'}), + y: md5.Sum([]byte{'b'}), + wantEqual: false, + reason: "hash differs", + }, { + label: label + "/NilStringer", + x: new(fmt.Stringer), + y: nil, + wantEqual: false, + reason: "by default differing types are always inequal", + }, { + label: label + "/TarHeaders", + x: makeTarHeaders('0'), + y: makeTarHeaders('\x00'), + wantEqual: false, + reason: "type flag differs between the headers", + }, { + label: label + "/NonDeterministicComparer", + x: make([]int, 1000), + y: make([]int, 1000), + opts: []cmp.Option{ + cmp.Comparer(func(_, _ int) bool { + return rand.Intn(2) == 0 + }), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + reason: "non-deterministic comparer", + }, { + label: label + "/NonDeterministicFilter", + x: make([]int, 1000), + y: make([]int, 1000), + opts: []cmp.Option{ + cmp.FilterValues(func(_, _ int) bool { + return rand.Intn(2) == 0 + }, cmp.Ignore()), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + reason: "non-deterministic filter", + }, { + label: label + "/AssymetricComparer", + x: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + y: []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y int) bool { + return x < y + }), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + reason: "asymmetric comparer", + }, { + label: label + "/NonDeterministicTransformer", + x: make([]string, 1000), + y: make([]string, 1000), + opts: []cmp.Option{ + cmp.Transformer("λ", func(x string) int { + return rand.Int() + }), + }, + wantPanic: "non-deterministic function detected", + reason: "non-deterministic transformer", + }, { + label: label + "/IrreflexiveComparison", + x: make([]int, 10), + y: make([]int, 10), + opts: []cmp.Option{ + cmp.Transformer("λ", func(x int) float64 { + return math.NaN() + }), + }, + wantEqual: false, + reason: "dynamic checks should not panic for non-reflexive comparisons", + }, { + label: label + "/StringerMapKey", + x: map[*pb.Stringer]*pb.Stringer{{"hello"}: {"world"}}, + y: map[*pb.Stringer]*pb.Stringer(nil), + wantEqual: false, + reason: "stringer should be used to format the map key", + }, { + label: label + "/StringerBacktick", + x: []*pb.Stringer{{`multi\nline\nline\nline`}}, + wantEqual: false, + reason: "stringer should use backtick quoting if more readable", + }, { + label: label + "/AvoidPanicAssignableConverter", + x: struct{ I Iface2 }{}, + y: struct{ I Iface2 }{}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y Iface1) bool { + return x == nil && y == nil + }), + }, + wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", + }, { + label: label + "/AvoidPanicAssignableTransformer", + x: struct{ I Iface2 }{}, + y: struct{ I Iface2 }{}, + opts: []cmp.Option{ + cmp.Transformer("λ", func(v Iface1) bool { + return v == nil + }), + }, + wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", + }, { + label: label + "/AvoidPanicAssignableFilter", + x: struct{ I Iface2 }{}, + y: struct{ I Iface2 }{}, + opts: []cmp.Option{ + cmp.FilterValues(func(x, y Iface1) bool { + return x == nil && y == nil + }, cmp.Ignore()), + }, + wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", + }, { + label: label + "/DynamicMap", + x: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63, "name": "Sammy Sosa"}}, + y: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65.0, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63.0, "name": "Sammy Sosa"}}, + wantEqual: false, + reason: "dynamic map with differing types (but semantically equivalent values) should be inequal", + }, { + label: label + "/MapKeyPointer", + x: map[*int]string{ + new(int): "hello", + }, + y: map[*int]string{ + new(int): "world", + }, + wantEqual: false, + reason: "map keys should use shallow (rather than deep) pointer comparison", + }, { + label: label + "/IgnoreSliceElements", + x: [2][]int{ + {0, 0, 0, 1, 2, 3, 0, 0, 4, 5, 6, 7, 8, 0, 9, 0, 0}, + {0, 1, 0, 0, 0, 20}, + }, + y: [2][]int{ + {1, 2, 3, 0, 4, 5, 6, 7, 0, 8, 9, 0, 0, 0}, + {0, 0, 1, 2, 0, 0, 0}, + }, + opts: []cmp.Option{ + cmp.FilterPath(func(p cmp.Path) bool { + vx, vy := p.Last().Values() + if vx.IsValid() && vx.Kind() == reflect.Int && vx.Int() == 0 { + return true + } + if vy.IsValid() && vy.Kind() == reflect.Int && vy.Int() == 0 { + return true + } + return false + }, cmp.Ignore()), + }, + wantEqual: false, + reason: "all zero slice elements are ignored (even if missing)", + }, { + label: label + "/IgnoreMapEntries", + x: [2]map[string]int{ + {"ignore1": 0, "ignore2": 0, "keep1": 1, "keep2": 2, "KEEP3": 3, "IGNORE3": 0}, + {"keep1": 1, "ignore1": 0}, + }, + y: [2]map[string]int{ + {"ignore1": 0, "ignore3": 0, "ignore4": 0, "keep1": 1, "keep2": 2, "KEEP3": 3}, + {"keep1": 1, "keep2": 2, "ignore2": 0}, + }, + opts: []cmp.Option{ + cmp.FilterPath(func(p cmp.Path) bool { + vx, vy := p.Last().Values() + if vx.IsValid() && vx.Kind() == reflect.Int && vx.Int() == 0 { + return true + } + if vy.IsValid() && vy.Kind() == reflect.Int && vy.Int() == 0 { + return true + } + return false + }, cmp.Ignore()), + }, + wantEqual: false, + reason: "all zero map entries are ignored (even if missing)", + }, { + label: label + "/PanicUnexportedNamed", + x: namedWithUnexported{}, + y: namedWithUnexported{}, + wantPanic: strconv.Quote(reflect.TypeOf(namedWithUnexported{}).PkgPath()) + ".namedWithUnexported", + reason: "panic on named struct type with unexported field", + }, { + label: label + "/PanicUnexportedUnnamed", + x: struct{ a int }{}, + y: struct{ a int }{}, + wantPanic: strconv.Quote(reflect.TypeOf(namedWithUnexported{}).PkgPath()) + ".(struct { a int })", + reason: "panic on unnamed struct type with unexported field", + }, { + label: label + "/UnaddressableStruct", + x: struct{ s fmt.Stringer }{new(bytes.Buffer)}, + y: struct{ s fmt.Stringer }{nil}, + opts: []cmp.Option{ + cmp.AllowUnexported(struct{ s fmt.Stringer }{}), + cmp.FilterPath(func(p cmp.Path) bool { + if _, ok := p.Last().(cmp.StructField); !ok { + return false + } + + t := p.Index(-1).Type() + vx, vy := p.Index(-1).Values() + pvx, pvy := p.Index(-2).Values() + switch { + case vx.Type() != t: + panic(fmt.Sprintf("inconsistent type: %v != %v", vx.Type(), t)) + case vy.Type() != t: + panic(fmt.Sprintf("inconsistent type: %v != %v", vy.Type(), t)) + case vx.CanAddr() != pvx.CanAddr(): + panic(fmt.Sprintf("inconsistent addressability: %v != %v", vx.CanAddr(), pvx.CanAddr())) + case vy.CanAddr() != pvy.CanAddr(): + panic(fmt.Sprintf("inconsistent addressability: %v != %v", vy.CanAddr(), pvy.CanAddr())) + } + return true + }, cmp.Ignore()), + }, + wantEqual: true, + reason: "verify that exporter does not leak implementation details", + }, { + label: label + "/ErrorPanic", + x: io.EOF, + y: io.EOF, + wantPanic: "consider using cmpopts.EquateErrors", + reason: "suggest cmpopts.EquateErrors when accessing unexported fields of error types", + }, { + label: label + "/ErrorEqual", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{cmpopts.EquateErrors()}, + wantEqual: true, + reason: "cmpopts.EquateErrors should equate these two errors as sentinel values", + }} +} + +func transformerTests() []test { + type StringBytes struct { + String string + Bytes []byte + } + + const label = "Transformer" + + transformOnce := func(name string, f interface{}) cmp.Option { + xform := cmp.Transformer(name, f) + return cmp.FilterPath(func(p cmp.Path) bool { + for _, ps := range p { + if tr, ok := ps.(cmp.Transform); ok && tr.Option() == xform { + return false + } + } + return true + }, xform) + } + + return []test{{ + label: label + "/Uints", + x: uint8(0), + y: uint8(1), + opts: []cmp.Option{ + cmp.Transformer("λ", func(in uint8) uint16 { return uint16(in) }), + cmp.Transformer("λ", func(in uint16) uint32 { return uint32(in) }), + cmp.Transformer("λ", func(in uint32) uint64 { return uint64(in) }), + }, + wantEqual: false, + reason: "transform uint8 -> uint16 -> uint32 -> uint64", + }, { + label: label + "/Ambiguous", + x: 0, + y: 1, + opts: []cmp.Option{ + cmp.Transformer("λ", func(in int) int { return in / 2 }), + cmp.Transformer("λ", func(in int) int { return in }), + }, + wantPanic: "ambiguous set of applicable options", + reason: "both transformers apply on int", + }, { + label: label + "/Filtered", + x: []int{0, -5, 0, -1}, + y: []int{1, 3, 0, -5}, + opts: []cmp.Option{ + cmp.FilterValues( + func(x, y int) bool { return x+y >= 0 }, + cmp.Transformer("λ", func(in int) int64 { return int64(in / 2) }), + ), + cmp.FilterValues( + func(x, y int) bool { return x+y < 0 }, + cmp.Transformer("λ", func(in int) int64 { return int64(in) }), + ), + }, + wantEqual: false, + reason: "disjoint transformers filtered based on the values", + }, { + label: label + "/DisjointOutput", + x: 0, + y: 1, + opts: []cmp.Option{ + cmp.Transformer("λ", func(in int) interface{} { + if in == 0 { + return "zero" + } + return float64(in) + }), + }, + wantEqual: false, + reason: "output type differs based on input value", + }, { + label: label + "/JSON", + x: `{ + "firstName": "John", + "lastName": "Smith", + "age": 25, + "isAlive": true, + "address": { + "city": "Los Angeles", + "postalCode": "10021-3100", + "state": "CA", + "streetAddress": "21 2nd Street" + }, + "phoneNumbers": [{ + "type": "home", + "number": "212 555-4321" + },{ + "type": "office", + "number": "646 555-4567" + },{ + "number": "123 456-7890", + "type": "mobile" + }], + "children": [] + }`, + y: `{"firstName":"John","lastName":"Smith","isAlive":true,"age":25, + "address":{"streetAddress":"21 2nd Street","city":"New York", + "state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home", + "number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{ + "type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`, + opts: []cmp.Option{ + transformOnce("ParseJSON", func(s string) (m map[string]interface{}) { + if err := json.Unmarshal([]byte(s), &m); err != nil { + panic(err) + } + return m + }), + }, + wantEqual: false, + reason: "transformer used to parse JSON input", + }, { + label: label + "/AcyclicString", + x: StringBytes{String: "some\nmulti\nLine\nstring", Bytes: []byte("some\nmulti\nline\nbytes")}, + y: StringBytes{String: "some\nmulti\nline\nstring", Bytes: []byte("some\nmulti\nline\nBytes")}, + opts: []cmp.Option{ + transformOnce("SplitString", func(s string) []string { return strings.Split(s, "\n") }), + transformOnce("SplitBytes", func(b []byte) [][]byte { return bytes.Split(b, []byte("\n")) }), + }, + wantEqual: false, + reason: "string -> []string and []byte -> [][]byte transformer only applied once", + }, { + label: label + "/CyclicString", + x: "a\nb\nc\n", + y: "a\nb\nc\n", + opts: []cmp.Option{ + cmp.Transformer("SplitLines", func(s string) []string { return strings.Split(s, "\n") }), + }, + wantPanic: "recursive set of Transformers detected", + reason: "cyclic transformation from string -> []string -> string", + }, { + label: label + "/CyclicComplex", + x: complex64(0), + y: complex64(0), + opts: []cmp.Option{ + cmp.Transformer("T1", func(x complex64) complex128 { return complex128(x) }), + cmp.Transformer("T2", func(x complex128) [2]float64 { return [2]float64{real(x), imag(x)} }), + cmp.Transformer("T3", func(x float64) complex64 { return complex64(complex(x, 0)) }), + }, + wantPanic: "recursive set of Transformers detected", + reason: "cyclic transformation from complex64 -> complex128 -> [2]float64 -> complex64", + }} +} + +func reporterTests() []test { + const label = "Reporter" + + type ( + MyString string + MyByte byte + MyBytes []byte + MyInt int8 + MyInts []int8 + MyUint int16 + MyUints []int16 + MyFloat float32 + MyFloats []float32 + MyComposite struct { + StringA string + StringB MyString + BytesA []byte + BytesB []MyByte + BytesC MyBytes + IntsA []int8 + IntsB []MyInt + IntsC MyInts + UintsA []uint16 + UintsB []MyUint + UintsC MyUints + FloatsA []float32 + FloatsB []MyFloat + FloatsC MyFloats + } + ) + + return []test{{ + label: label + "/PanicStringer", + x: struct{ X fmt.Stringer }{struct{ fmt.Stringer }{nil}}, + y: struct{ X fmt.Stringer }{bytes.NewBuffer(nil)}, + wantEqual: false, + reason: "panic from fmt.Stringer should not crash the reporter", + }, { + label: label + "/PanicError", + x: struct{ X error }{struct{ error }{nil}}, + y: struct{ X error }{errors.New("")}, + wantEqual: false, + reason: "panic from error should not crash the reporter", + }, { + label: label + "/AmbiguousType", + x: foo1.Bar{}, + y: foo2.Bar{}, + wantEqual: false, + reason: "reporter should display the qualified type name to disambiguate between the two values", + }, { + label: label + "/AmbiguousPointer", + x: newInt(0), + y: newInt(0), + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two values", + }, { + label: label + "/AmbiguousPointerStruct", + x: struct{ I *int }{newInt(0)}, + y: struct{ I *int }{newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two struct fields", + }, { + label: label + "/AmbiguousPointerSlice", + x: []*int{newInt(0)}, + y: []*int{newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two slice elements", + }, { + label: label + "/AmbiguousPointerMap", + x: map[string]*int{"zero": newInt(0)}, + y: map[string]*int{"zero": newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two map values", + }, { + label: label + "/AmbiguousStringer", + x: Stringer("hello"), + y: newStringer("hello"), + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two values", + }, { + label: label + "/AmbiguousStringerStruct", + x: struct{ S fmt.Stringer }{Stringer("hello")}, + y: struct{ S fmt.Stringer }{newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two struct fields", + }, { + label: label + "/AmbiguousStringerSlice", + x: []fmt.Stringer{Stringer("hello")}, + y: []fmt.Stringer{newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two slice elements", + }, { + label: label + "/AmbiguousStringerMap", + x: map[string]fmt.Stringer{"zero": Stringer("hello")}, + y: map[string]fmt.Stringer{"zero": newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two map values", + }, { + label: label + "/AmbiguousSliceHeader", + x: make([]int, 0, 5), + y: make([]int, 0, 1000), + opts: []cmp.Option{ + cmp.Comparer(func(x, y []int) bool { return cap(x) == cap(y) }), + }, + wantEqual: false, + reason: "reporter should display the slice header to disambiguate between the two slice values", + }, { + label: label + "/AmbiguousStringerMapKey", + x: map[interface{}]string{ + nil: "nil", + Stringer("hello"): "goodbye", + foo1.Bar{"fizz"}: "buzz", + }, + y: map[interface{}]string{ + newStringer("hello"): "goodbye", + foo2.Bar{"fizz"}: "buzz", + }, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two map keys", + }, { + label: label + "/NonAmbiguousStringerMapKey", + x: map[interface{}]string{Stringer("hello"): "goodbye"}, + y: map[interface{}]string{newStringer("fizz"): "buzz"}, + wantEqual: false, + reason: "reporter should call String as there is no ambiguity between the two map keys", + }, { + label: label + "/InvalidUTF8", + x: MyString("\xed\xa0\x80"), + wantEqual: false, + reason: "invalid UTF-8 should format as quoted string", + }, { + label: label + "/UnbatchedSlice", + x: MyComposite{IntsA: []int8{11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + y: MyComposite{IntsA: []int8{10, 11, 21, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + wantEqual: false, + reason: "unbatched diffing desired since few elements differ", + }, { + label: label + "/BatchedSlice", + x: MyComposite{IntsA: []int8{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + y: MyComposite{IntsA: []int8{12, 29, 13, 27, 22, 23, 17, 18, 19, 20, 21, 10, 26, 16, 25, 28, 11, 15, 24, 14}}, + wantEqual: false, + reason: "batched diffing desired since many elements differ", + }, { + label: label + "/BatchedWithComparer", + x: MyComposite{BytesA: []byte{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + y: MyComposite{BytesA: []byte{12, 29, 13, 27, 22, 23, 17, 18, 19, 20, 21, 10, 26, 16, 25, 28, 11, 15, 24, 14}}, + wantEqual: false, + opts: []cmp.Option{ + cmp.Comparer(bytes.Equal), + }, + reason: "batched diffing desired since many elements differ", + }, { + label: label + "/BatchedLong", + x: MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127}}, + wantEqual: false, + reason: "batched output desired for a single slice of primitives unique to one of the inputs", + }, { + label: label + "/BatchedNamedAndUnnamed", + x: MyComposite{ + BytesA: []byte{1, 2, 3}, + BytesB: []MyByte{4, 5, 6}, + BytesC: MyBytes{7, 8, 9}, + IntsA: []int8{-1, -2, -3}, + IntsB: []MyInt{-4, -5, -6}, + IntsC: MyInts{-7, -8, -9}, + UintsA: []uint16{1000, 2000, 3000}, + UintsB: []MyUint{4000, 5000, 6000}, + UintsC: MyUints{7000, 8000, 9000}, + FloatsA: []float32{1.5, 2.5, 3.5}, + FloatsB: []MyFloat{4.5, 5.5, 6.5}, + FloatsC: MyFloats{7.5, 8.5, 9.5}, + }, + y: MyComposite{ + BytesA: []byte{3, 2, 1}, + BytesB: []MyByte{6, 5, 4}, + BytesC: MyBytes{9, 8, 7}, + IntsA: []int8{-3, -2, -1}, + IntsB: []MyInt{-6, -5, -4}, + IntsC: MyInts{-9, -8, -7}, + UintsA: []uint16{3000, 2000, 1000}, + UintsB: []MyUint{6000, 5000, 4000}, + UintsC: MyUints{9000, 8000, 7000}, + FloatsA: []float32{3.5, 2.5, 1.5}, + FloatsB: []MyFloat{6.5, 5.5, 4.5}, + FloatsC: MyFloats{9.5, 8.5, 7.5}, + }, + wantEqual: false, + reason: "batched diffing available for both named and unnamed slices", + }, { + label: label + "/BinaryHexdump", + x: MyComposite{BytesA: []byte("\xf3\x0f\x8a\xa4\xd3\x12R\t$\xbeX\x95A\xfd$fX\x8byT\xac\r\xd8qwp\x20j\\s\u007f\x8c\x17U\xc04\xcen\xf7\xaaG\xee2\x9d\xc5\xca\x1eX\xaf\x8f'\xf3\x02J\x90\xedi.p2\xb4\xab0 \xb6\xbd\\b4\x17\xb0\x00\xbbO~'G\x06\xf4.f\xfdc\xd7\x04ݷ0\xb7\xd1U~{\xf6\xb3~\x1dWi \x9e\xbc\xdf\xe1M\xa9\xef\xa2\xd2\xed\xb4Gx\xc9\xc9'\xa4\xc6\xce\xecDp]")}, + y: MyComposite{BytesA: []byte("\xf3\x0f\x8a\xa4\xd3\x12R\t$\xbeT\xac\r\xd8qwp\x20j\\s\u007f\x8c\x17U\xc04\xcen\xf7\xaaG\xee2\x9d\xc5\xca\x1eX\xaf\x8f'\xf3\x02J\x90\xedi.p2\xb4\xab0 \xb6\xbd\\b4\x17\xb0\x00\xbbO~'G\x06\xf4.f\xfdc\xd7\x04ݷ0\xb7\xd1u-[]]\xf6\xb3haha~\x1dWI \x9e\xbc\xdf\xe1M\xa9\xef\xa2\xd2\xed\xb4Gx\xc9\xc9'\xa4\xc6\xce\xecDp]")}, + wantEqual: false, + reason: "binary diff in hexdump form since data is binary data", + }, { + label: label + "/StringHexdump", + x: MyComposite{StringB: MyString("readme.txt\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000600\x000000000\x000000000\x0000000000046\x0000000000000\x00011173\x00 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ustar\x0000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000000\x000000000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, + y: MyComposite{StringB: MyString("gopher.txt\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000600\x000000000\x000000000\x0000000000043\x0000000000000\x00011217\x00 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ustar\x0000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000000\x000000000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, + wantEqual: false, + reason: "binary diff desired since string looks like binary data", + }, { + label: label + "/BinaryString", + x: MyComposite{BytesA: []byte(`{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"314 54th Avenue","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`)}, + y: MyComposite{BytesA: []byte(`{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"21 2nd Street","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`)}, + wantEqual: false, + reason: "batched textual diff desired since bytes looks like textual data", + }, { + label: label + "/TripleQuote", + x: MyComposite{StringA: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n"}, + y: MyComposite{StringA: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n"}, + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/TripleQuoteSlice", + x: []string{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + y: []string{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + wantEqual: false, + reason: "use triple-quote syntax for slices of strings", + }, { + label: label + "/TripleQuoteNamedTypes", + x: MyComposite{ + StringB: MyString("aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + BytesC: MyBytes("aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + }, + y: MyComposite{ + StringB: MyString("aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + BytesC: MyBytes("aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + }, + wantEqual: false, + reason: "use triple-quote syntax for named types", + }, { + label: label + "/TripleQuoteSliceNamedTypes", + x: []MyString{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + y: []MyString{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + wantEqual: false, + reason: "use triple-quote syntax for slices of named strings", + }, { + label: label + "/TripleQuoteEndlines", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\r\nhhh\n\riii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n\r", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\r\nhhh\n\riii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz", + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/AvoidTripleQuoteAmbiguousQuotes", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\n\"\"\"\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to presence of ambiguous triple quotes", + }, { + label: label + "/AvoidTripleQuoteAmbiguousEllipsis", + x: "aaa\nbbb\nccc\n...\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to presence of ambiguous ellipsis", + }, { + label: label + "/AvoidTripleQuoteNonPrintable", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\no\roo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/AvoidTripleQuoteIdenticalWhitespace", + x: "aaa\nbbb\nccc\n ddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nccc \nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to visual equivalence of differences", + }, { + label: label + "/TripleQuoteStringer", + x: []fmt.Stringer{ + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")), + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n)\n\nfunc main() {\n\tfmt.Println(\"My favorite number is\", rand.Intn(10))\n}\n")), + }, + y: []fmt.Stringer{ + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")), + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\nfunc main() {\n\tfmt.Printf(\"Now you have %g problems.\\n\", math.Sqrt(7))\n}\n")), + }, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + wantEqual: false, + reason: "multi-line String output should be formatted with triple quote", + }, { + label: label + "/LimitMaximumBytesDiffs", + x: []byte("\xcd====\x06\x1f\xc2\xcc\xc2-S=====\x1d\xdfa\xae\x98\x9fH======ǰ\xb7=======\xef====:\\\x94\xe6J\xc7=====\xb4======\n\n\xf7\x94===========\xf2\x9c\xc0f=====4\xf6\xf1\xc3\x17\x82======n\x16`\x91D\xc6\x06=======\x1cE====.===========\xc4\x18=======\x8a\x8d\x0e====\x87\xb1\xa5\x8e\xc3=====z\x0f1\xaeU======G,=======5\xe75\xee\x82\xf4\xce====\x11r===========\xaf]=======z\x05\xb3\x91\x88%\xd2====\n1\x89=====i\xb7\x055\xe6\x81\xd2=============\x883=@̾====\x14\x05\x96%^t\x04=====\xe7Ȉ\x90\x1d============="), + y: []byte("\\====|\x96\xe7SB\xa0\xab=====\xf0\xbd\xa5q\xab\x17;======\xabP\x00=======\xeb====\xa5\x14\xe6O(\xe4=====(======/c@?===========\xd9x\xed\x13=====J\xfc\x918B\x8d======a8A\xebs\x04\xae=======\aC====\x1c===========\x91\"=======uؾ====s\xec\x845\a=====;\xabS9t======\x1f\x1b=======\x80\xab/\xed+:;====\xeaI===========\xabl=======\xb9\xe9\xfdH\x93\x8e\u007f====ח\xe5=====Ig\x88m\xf5\x01V=============\xf7+4\xb0\x92E====\x9fj\xf8&\xd0h\xf9=====\xeeΨ\r\xbf============="), + wantEqual: false, + reason: "total bytes difference output is truncated due to excessive number of differences", + }, { + label: label + "/LimitMaximumStringDiffs", + x: "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", + y: "aa\nb\ncc\nd\nee\nf\ngg\nh\nii\nj\nkk\nl\nmm\nn\noo\np\nqq\nr\nss\nt\nuu\nv\nww\nx\nyy\nz\nAA\nB\nCC\nD\nEE\nF\nGG\nH\nII\nJ\nKK\nL\nMM\nN\nOO\nP\nQQ\nR\nSS\nT\nUU\nV\nWW\nX\nYY\nZ\n", + wantEqual: false, + reason: "total string difference output is truncated due to excessive number of differences", + }, { + label: label + "/LimitMaximumSliceDiffs", + x: func() (out []struct{ S string }) { + for _, s := range strings.Split("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", "\n") { + out = append(out, struct{ S string }{s}) + } + return out + }(), + y: func() (out []struct{ S string }) { + for _, s := range strings.Split("aa\nb\ncc\nd\nee\nf\ngg\nh\nii\nj\nkk\nl\nmm\nn\noo\np\nqq\nr\nss\nt\nuu\nv\nww\nx\nyy\nz\nAA\nB\nCC\nD\nEE\nF\nGG\nH\nII\nJ\nKK\nL\nMM\nN\nOO\nP\nQQ\nR\nSS\nT\nUU\nV\nWW\nX\nYY\nZ\n", "\n") { + out = append(out, struct{ S string }{s}) + } + return out + }(), + wantEqual: false, + reason: "total slice difference output is truncated due to excessive number of differences", + }, { + label: label + "/MultilineString", + x: MyComposite{ + StringA: strings.TrimPrefix(` +Package cmp determines equality of values. + +This package is intended to be a more powerful and safer alternative to +reflect.DeepEqual for comparing whether two values are semantically equal. + +The primary features of cmp are: + +• When the default behavior of equality does not suit the needs of the test, +custom equality functions can override the equality operation. +For example, an equality function may report floats as equal so long as they +are within some tolerance of each other. + +• Types that have an Equal method may use that method to determine equality. +This allows package authors to determine the equality operation for the types +that they define. + +• If no custom equality functions are used and no Equal method is defined, +equality is determined by recursively comparing the primitive kinds on both +values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported +fields are not compared by default; they result in panics unless suppressed +by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared +using the AllowUnexported option. +`, "\n"), + }, + y: MyComposite{ + StringA: strings.TrimPrefix(` +Package cmp determines equality of value. + +This package is intended to be a more powerful and safer alternative to +reflect.DeepEqual for comparing whether two values are semantically equal. + +The primary features of cmp are: + +• When the default behavior of equality does not suit the needs of the test, +custom equality functions can override the equality operation. +For example, an equality function may report floats as equal so long as they +are within some tolerance of each other. + +• If no custom equality functions are used and no Equal method is defined, +equality is determined by recursively comparing the primitive kinds on both +values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported +fields are not compared by default; they result in panics unless suppressed +by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared +using the AllowUnexported option.`, "\n"), + }, + wantEqual: false, + reason: "batched per-line diff desired since string looks like multi-line textual data", + }, { + label: label + "/Slices", + x: MyComposite{ + BytesA: []byte{1, 2, 3}, + BytesB: []MyByte{4, 5, 6}, + BytesC: MyBytes{7, 8, 9}, + IntsA: []int8{-1, -2, -3}, + IntsB: []MyInt{-4, -5, -6}, + IntsC: MyInts{-7, -8, -9}, + UintsA: []uint16{1000, 2000, 3000}, + UintsB: []MyUint{4000, 5000, 6000}, + UintsC: MyUints{7000, 8000, 9000}, + FloatsA: []float32{1.5, 2.5, 3.5}, + FloatsB: []MyFloat{4.5, 5.5, 6.5}, + FloatsC: MyFloats{7.5, 8.5, 9.5}, + }, + y: MyComposite{}, + wantEqual: false, + reason: "batched diffing for non-nil slices and nil slices", + }, { + label: label + "/EmptySlices", + x: MyComposite{ + BytesA: []byte{}, + BytesB: []MyByte{}, + BytesC: MyBytes{}, + IntsA: []int8{}, + IntsB: []MyInt{}, + IntsC: MyInts{}, + UintsA: []uint16{}, + UintsB: []MyUint{}, + UintsC: MyUints{}, + FloatsA: []float32{}, + FloatsB: []MyFloat{}, + FloatsC: MyFloats{}, + }, + y: MyComposite{}, + wantEqual: false, + reason: "batched diffing for empty slices and nil slices", + }, { + label: label + "/LargeMapKey", + x: map[*[]byte]int{func() *[]byte { + b := make([]byte, 1<<20, 1<<20) + return &b + }(): 0}, + y: map[*[]byte]int{func() *[]byte { + b := make([]byte, 1<<20, 1<<20) + return &b + }(): 0}, + reason: "printing map keys should have some verbosity limit imposed", + }, { + label: label + "/LargeStringInInterface", + x: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}, + y: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,"}, + reason: "strings within an interface should benefit from specialized diffing", + }, { + label: label + "/LargeBytesInInterface", + x: struct{ X interface{} }{[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis.")}, + y: struct{ X interface{} }{[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,")}, + reason: "bytes slice within an interface should benefit from specialized diffing", + }, { + label: label + "/LargeStandaloneString", + x: struct{ X interface{} }{[1]string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}}, + y: struct{ X interface{} }{[1]string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,"}}, + reason: "printing a large standalone string that is different should print enough context to see the difference", + }} +} + +func embeddedTests() []test { + const label = "EmbeddedStruct" + + privateStruct := *new(ts.ParentStructA).PrivateStruct() + + createStructA := func(i int) ts.ParentStructA { + s := ts.ParentStructA{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + return s + } + + createStructB := func(i int) ts.ParentStructB { + s := ts.ParentStructB{} + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + return s + } + + createStructC := func(i int) ts.ParentStructC { + s := ts.ParentStructC{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.Public = 3 + i + s.SetPrivate(4 + i) + return s + } + + createStructD := func(i int) ts.ParentStructD { + s := ts.ParentStructD{} + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + s.Public = 3 + i + s.SetPrivate(4 + i) + return s + } + + createStructE := func(i int) ts.ParentStructE { + s := ts.ParentStructE{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + return s + } + + createStructF := func(i int) ts.ParentStructF { + s := ts.ParentStructF{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + s.Public = 5 + i + s.SetPrivate(6 + i) + return s + } + + createStructG := func(i int) *ts.ParentStructG { + s := ts.NewParentStructG() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + return s + } + + createStructH := func(i int) *ts.ParentStructH { + s := ts.NewParentStructH() + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + return s + } + + createStructI := func(i int) *ts.ParentStructI { + s := ts.NewParentStructI() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + return s + } + + createStructJ := func(i int) *ts.ParentStructJ { + s := ts.NewParentStructJ() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + s.Private().Public = 5 + i + s.Private().SetPrivate(6 + i) + s.Public.Public = 7 + i + s.Public.SetPrivate(8 + i) + return s + } + + // TODO(≥go1.10): Workaround for reflect bug (https://golang.org/issue/21122). + wantPanicNotGo110 := func(s string) string { + if !flags.AtLeastGo110 { + return "" + } + return s + } + + return []test{{ + label: label + "/ParentStructA/PanicUnexported1", + x: ts.ParentStructA{}, + y: ts.ParentStructA{}, + wantPanic: "cannot handle unexported field", + reason: "ParentStructA has an unexported field", + }, { + label: label + "/ParentStructA/Ignored", + x: ts.ParentStructA{}, + y: ts.ParentStructA{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructA{}), + }, + wantEqual: true, + reason: "the only field (which is unexported) of ParentStructA is ignored", + }, { + label: label + "/ParentStructA/PanicUnexported2", + x: createStructA(0), + y: createStructA(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}), + }, + wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", + }, { + label: label + "/ParentStructA/Equal", + x: createStructA(0), + y: createStructA(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructA and privateStruct are allowed", + }, { + label: label + "/ParentStructA/Inequal", + x: createStructA(0), + y: createStructA(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructB/PanicUnexported1", + x: ts.ParentStructB{}, + y: ts.ParentStructB{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructB{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct has an unexported field", + }, { + label: label + "/ParentStructB/Ignored", + x: ts.ParentStructB{}, + y: ts.ParentStructB{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructB{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructB and PublicStruct are ignored", + }, { + label: label + "/ParentStructB/PanicUnexported2", + x: createStructB(0), + y: createStructB(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", + }, { + label: label + "/ParentStructB/Equal", + x: createStructB(0), + y: createStructB(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructB and PublicStruct are allowed", + }, { + label: label + "/ParentStructB/Inequal", + x: createStructB(0), + y: createStructB(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructC/PanicUnexported1", + x: ts.ParentStructC{}, + y: ts.ParentStructC{}, + wantPanic: "cannot handle unexported field", + reason: "ParentStructC has unexported fields", + }, { + label: label + "/ParentStructC/Ignored", + x: ts.ParentStructC{}, + y: ts.ParentStructC{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructC{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructC are ignored", + }, { + label: label + "/ParentStructC/PanicUnexported2", + x: createStructC(0), + y: createStructC(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}), + }, + wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", + }, { + label: label + "/ParentStructC/Equal", + x: createStructC(0), + y: createStructC(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructC and privateStruct are allowed", + }, { + label: label + "/ParentStructC/Inequal", + x: createStructC(0), + y: createStructC(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructD/PanicUnexported1", + x: ts.ParentStructD{}, + y: ts.ParentStructD{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructD{}), + }, + wantPanic: "cannot handle unexported field", + reason: "ParentStructD has unexported fields", + }, { + label: label + "/ParentStructD/Ignored", + x: ts.ParentStructD{}, + y: ts.ParentStructD{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructD{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructD and PublicStruct are ignored", + }, { + label: label + "/ParentStructD/PanicUnexported2", + x: createStructD(0), + y: createStructD(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", + }, { + label: label + "/ParentStructD/Equal", + x: createStructD(0), + y: createStructD(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructD and PublicStruct are allowed", + }, { + label: label + "/ParentStructD/Inequal", + x: createStructD(0), + y: createStructD(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructE/PanicUnexported1", + x: ts.ParentStructE{}, + y: ts.ParentStructE{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructE{}), + }, + wantPanic: "cannot handle unexported field", + reason: "ParentStructE has unexported fields", + }, { + label: label + "/ParentStructE/Ignored", + x: ts.ParentStructE{}, + y: ts.ParentStructE{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructE{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructE and PublicStruct are ignored", + }, { + label: label + "/ParentStructE/PanicUnexported2", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", + }, { + label: label + "/ParentStructE/PanicUnexported3", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", + }, { + label: label + "/ParentStructE/Equal", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructE, PublicStruct, and privateStruct are allowed", + }, { + label: label + "/ParentStructE/Inequal", + x: createStructE(0), + y: createStructE(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructF/PanicUnexported1", + x: ts.ParentStructF{}, + y: ts.ParentStructF{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructF{}), + }, + wantPanic: "cannot handle unexported field", + reason: "ParentStructF has unexported fields", + }, { + label: label + "/ParentStructF/Ignored", + x: ts.ParentStructF{}, + y: ts.ParentStructF{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructF{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructF and PublicStruct are ignored", + }, { + label: label + "/ParentStructF/PanicUnexported2", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", + }, { + label: label + "/ParentStructF/PanicUnexported3", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", + }, { + label: label + "/ParentStructF/Equal", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructF, PublicStruct, and privateStruct are allowed", + }, { + label: label + "/ParentStructF/Inequal", + x: createStructF(0), + y: createStructF(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructG/PanicUnexported1", + x: ts.ParentStructG{}, + y: ts.ParentStructG{}, + wantPanic: wantPanicNotGo110("cannot handle unexported field"), + wantEqual: !flags.AtLeastGo110, + reason: "ParentStructG has unexported fields", + }, { + label: label + "/ParentStructG/Ignored", + x: ts.ParentStructG{}, + y: ts.ParentStructG{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructG{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructG are ignored", + }, { + label: label + "/ParentStructG/PanicUnexported2", + x: createStructG(0), + y: createStructG(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}), + }, + wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", + }, { + label: label + "/ParentStructG/Equal", + x: createStructG(0), + y: createStructG(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructG and privateStruct are allowed", + }, { + label: label + "/ParentStructG/Inequal", + x: createStructG(0), + y: createStructG(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructH/EqualNil", + x: ts.ParentStructH{}, + y: ts.ParentStructH{}, + wantEqual: true, + reason: "PublicStruct is not compared because the pointer is nil", + }, { + label: label + "/ParentStructH/PanicUnexported1", + x: createStructH(0), + y: createStructH(0), + wantPanic: "cannot handle unexported field", + reason: "PublicStruct has unexported fields", + }, { + label: label + "/ParentStructH/Ignored", + x: ts.ParentStructH{}, + y: ts.ParentStructH{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructH{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructH are ignored (it has none)", + }, { + label: label + "/ParentStructH/PanicUnexported2", + x: createStructH(0), + y: createStructH(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", + }, { + label: label + "/ParentStructH/Equal", + x: createStructH(0), + y: createStructH(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructH and PublicStruct are allowed", + }, { + label: label + "/ParentStructH/Inequal", + x: createStructH(0), + y: createStructH(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructI/PanicUnexported1", + x: ts.ParentStructI{}, + y: ts.ParentStructI{}, + wantPanic: wantPanicNotGo110("cannot handle unexported field"), + wantEqual: !flags.AtLeastGo110, + reason: "ParentStructI has unexported fields", + }, { + label: label + "/ParentStructI/Ignored1", + x: ts.ParentStructI{}, + y: ts.ParentStructI{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructI are ignored", + }, { + label: label + "/ParentStructI/PanicUnexported2", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", + }, { + label: label + "/ParentStructI/Ignored2", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}, ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructI and PublicStruct are ignored", + }, { + label: label + "/ParentStructI/PanicUnexported3", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", + }, { + label: label + "/ParentStructI/Equal", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructI, PublicStruct, and privateStruct are allowed", + }, { + label: label + "/ParentStructI/Inequal", + x: createStructI(0), + y: createStructI(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructJ/PanicUnexported1", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + wantPanic: "cannot handle unexported field", + reason: "ParentStructJ has unexported fields", + }, { + label: label + "/ParentStructJ/PanicUnexported2", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructJ{}), + }, + wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", + }, { + label: label + "/ParentStructJ/Ignored", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), + }, + wantEqual: true, + reason: "unexported fields of ParentStructJ and PublicStruct are ignored", + }, { + label: label + "/ParentStructJ/PanicUnexported3", + x: createStructJ(0), + y: createStructJ(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", + }, { + label: label + "/ParentStructJ/Equal", + x: createStructJ(0), + y: createStructJ(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: true, + reason: "unexported fields of both ParentStructJ, PublicStruct, and privateStruct are allowed", + }, { + label: label + "/ParentStructJ/Inequal", + x: createStructJ(0), + y: createStructJ(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: false, + reason: "the two values differ on some fields", + }} +} + +func methodTests() []test { + const label = "EqualMethod" + + // A common mistake that the Equal method is on a pointer receiver, + // but only a non-pointer value is present in the struct. + // A transform can be used to forcibly reference the value. + addrTransform := cmp.FilterPath(func(p cmp.Path) bool { + if len(p) == 0 { + return false + } + t := p[len(p)-1].Type() + if _, ok := t.MethodByName("Equal"); ok || t.Kind() == reflect.Ptr { + return false + } + if m, ok := reflect.PtrTo(t).MethodByName("Equal"); ok { + tf := m.Func.Type() + return !tf.IsVariadic() && tf.NumIn() == 2 && tf.NumOut() == 1 && + tf.In(0).AssignableTo(tf.In(1)) && tf.Out(0) == reflect.TypeOf(true) + } + return false + }, cmp.Transformer("Addr", func(x interface{}) interface{} { + v := reflect.ValueOf(x) + vp := reflect.New(v.Type()) + vp.Elem().Set(v) + return vp.Interface() + })) + + // For each of these types, there is an Equal method defined, which always + // returns true, while the underlying data are fundamentally different. + // Since the method should be called, these are expected to be equal. + return []test{{ + label: label + "/StructA/ValueEqual", + x: ts.StructA{X: "NotEqual"}, + y: ts.StructA{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructA value called", + }, { + label: label + "/StructA/PointerEqual", + x: &ts.StructA{X: "NotEqual"}, + y: &ts.StructA{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructA pointer called", + }, { + label: label + "/StructB/ValueInequal", + x: ts.StructB{X: "NotEqual"}, + y: ts.StructB{X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructB value not called", + }, { + label: label + "/StructB/ValueAddrEqual", + x: ts.StructB{X: "NotEqual"}, + y: ts.StructB{X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform", + }, { + label: label + "/StructB/PointerEqual", + x: &ts.StructB{X: "NotEqual"}, + y: &ts.StructB{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructB pointer called", + }, { + label: label + "/StructC/ValueEqual", + x: ts.StructC{X: "NotEqual"}, + y: ts.StructC{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructC value called", + }, { + label: label + "/StructC/PointerEqual", + x: &ts.StructC{X: "NotEqual"}, + y: &ts.StructC{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructC pointer called", + }, { + label: label + "/StructD/ValueInequal", + x: ts.StructD{X: "NotEqual"}, + y: ts.StructD{X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructD value not called", + }, { + label: label + "/StructD/ValueAddrEqual", + x: ts.StructD{X: "NotEqual"}, + y: ts.StructD{X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructD pointer called due to shallow copy transform", + }, { + label: label + "/StructD/PointerEqual", + x: &ts.StructD{X: "NotEqual"}, + y: &ts.StructD{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructD pointer called", + }, { + label: label + "/StructE/ValueInequal", + x: ts.StructE{X: "NotEqual"}, + y: ts.StructE{X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructE value not called", + }, { + label: label + "/StructE/ValueAddrEqual", + x: ts.StructE{X: "NotEqual"}, + y: ts.StructE{X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructE pointer called due to shallow copy transform", + }, { + label: label + "/StructE/PointerEqual", + x: &ts.StructE{X: "NotEqual"}, + y: &ts.StructE{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructE pointer called", + }, { + label: label + "/StructF/ValueInequal", + x: ts.StructF{X: "NotEqual"}, + y: ts.StructF{X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructF value not called", + }, { + label: label + "/StructF/PointerEqual", + x: &ts.StructF{X: "NotEqual"}, + y: &ts.StructF{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructF pointer called", + }, { + label: label + "/StructA1/ValueEqual", + x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, + y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructA value called with equal X field", + }, { + label: label + "/StructA1/ValueInequal", + x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructA value called, but inequal X field", + }, { + label: label + "/StructA1/PointerEqual", + x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, + y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructA value called with equal X field", + }, { + label: label + "/StructA1/PointerInequal", + x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructA value called, but inequal X field", + }, { + label: label + "/StructB1/ValueEqual", + x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, + y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform with equal X field", + }, { + label: label + "/StructB1/ValueInequal", + x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: false, + reason: "Equal method on StructB pointer called due to shallow copy transform, but inequal X field", + }, { + label: label + "/StructB1/PointerEqual", + x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, + y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform with equal X field", + }, { + label: label + "/StructB1/PointerInequal", + x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: false, + reason: "Equal method on StructB pointer called due to shallow copy transform, but inequal X field", + }, { + label: label + "/StructC1/ValueEqual", + x: ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructC1 value called", + }, { + label: label + "/StructC1/PointerEqual", + x: &ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructC1 pointer called", + }, { + label: label + "/StructD1/ValueInequal", + x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructD1 value not called", + }, { + label: label + "/StructD1/PointerAddrEqual", + x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructD1 pointer called due to shallow copy transform", + }, { + label: label + "/StructD1/PointerEqual", + x: &ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructD1 pointer called", + }, { + label: label + "/StructE1/ValueInequal", + x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructE1 value not called", + }, { + label: label + "/StructE1/ValueAddrEqual", + x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructE1 pointer called due to shallow copy transform", + }, { + label: label + "/StructE1/PointerEqual", + x: &ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructE1 pointer called", + }, { + label: label + "/StructF1/ValueInequal", + x: ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructF1 value not called", + }, { + label: label + "/StructF1/PointerEqual", + x: &ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructF1 pointer called", + }, { + label: label + "/StructA2/ValueEqual", + x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, + y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructA pointer called with equal X field", + }, { + label: label + "/StructA2/ValueInequal", + x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructA pointer called, but inequal X field", + }, { + label: label + "/StructA2/PointerEqual", + x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, + y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructA pointer called with equal X field", + }, { + label: label + "/StructA2/PointerInequal", + x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructA pointer called, but inequal X field", + }, { + label: label + "/StructB2/ValueEqual", + x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, + y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructB pointer called with equal X field", + }, { + label: label + "/StructB2/ValueInequal", + x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructB pointer called, but inequal X field", + }, { + label: label + "/StructB2/PointerEqual", + x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, + y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructB pointer called with equal X field", + }, { + label: label + "/StructB2/PointerInequal", + x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructB pointer called, but inequal X field", + }, { + label: label + "/StructC2/ValueEqual", + x: ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructC2 value due to forwarded StructC pointer", + }, { + label: label + "/StructC2/PointerEqual", + x: &ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructC2 pointer due to forwarded StructC pointer", + }, { + label: label + "/StructD2/ValueEqual", + x: ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructD2 value due to forwarded StructD pointer", + }, { + label: label + "/StructD2/PointerEqual", + x: &ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructD2 pointer due to forwarded StructD pointer", + }, { + label: label + "/StructE2/ValueEqual", + x: ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructE2 value due to forwarded StructE pointer", + }, { + label: label + "/StructE2/PointerEqual", + x: &ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructE2 pointer due to forwarded StructE pointer", + }, { + label: label + "/StructF2/ValueEqual", + x: ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructF2 value due to forwarded StructF pointer", + }, { + label: label + "/StructF2/PointerEqual", + x: &ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructF2 pointer due to forwarded StructF pointer", + }, { + label: label + "/StructNo/Inequal", + x: ts.StructNo{X: "NotEqual"}, + y: ts.StructNo{X: "not_equal"}, + wantEqual: false, + reason: "Equal method not called since StructNo is not assignable to InterfaceA", + }, { + label: label + "/AssignA/Equal", + x: ts.AssignA(func() int { return 0 }), + y: ts.AssignA(func() int { return 1 }), + wantEqual: true, + reason: "Equal method called since named func is assignable to unnamed func", + }, { + label: label + "/AssignB/Equal", + x: ts.AssignB(struct{ A int }{0}), + y: ts.AssignB(struct{ A int }{1}), + wantEqual: true, + reason: "Equal method called since named struct is assignable to unnamed struct", + }, { + label: label + "/AssignC/Equal", + x: ts.AssignC(make(chan bool)), + y: ts.AssignC(make(chan bool)), + wantEqual: true, + reason: "Equal method called since named channel is assignable to unnamed channel", + }, { + label: label + "/AssignD/Equal", + x: ts.AssignD(make(chan bool)), + y: ts.AssignD(make(chan bool)), + wantEqual: true, + reason: "Equal method called since named channel is assignable to unnamed channel", + }} +} + +type ( + CycleAlpha struct { + Name string + Bravos map[string]*CycleBravo + } + CycleBravo struct { + ID int + Name string + Mods int + Alphas map[string]*CycleAlpha + } +) + +func cycleTests() []test { + const label = "Cycle" + + type ( + P *P + S []S + M map[int]M + ) + + makeGraph := func() map[string]*CycleAlpha { + v := map[string]*CycleAlpha{ + "Foo": &CycleAlpha{ + Name: "Foo", + Bravos: map[string]*CycleBravo{ + "FooBravo": &CycleBravo{ + Name: "FooBravo", + ID: 101, + Mods: 100, + Alphas: map[string]*CycleAlpha{ + "Foo": nil, // cyclic reference + }, + }, + }, + }, + "Bar": &CycleAlpha{ + Name: "Bar", + Bravos: map[string]*CycleBravo{ + "BarBuzzBravo": &CycleBravo{ + Name: "BarBuzzBravo", + ID: 102, + Mods: 2, + Alphas: map[string]*CycleAlpha{ + "Bar": nil, // cyclic reference + "Buzz": nil, // cyclic reference + }, + }, + "BuzzBarBravo": &CycleBravo{ + Name: "BuzzBarBravo", + ID: 103, + Mods: 0, + Alphas: map[string]*CycleAlpha{ + "Bar": nil, // cyclic reference + "Buzz": nil, // cyclic reference + }, + }, + }, + }, + "Buzz": &CycleAlpha{ + Name: "Buzz", + Bravos: map[string]*CycleBravo{ + "BarBuzzBravo": nil, // cyclic reference + "BuzzBarBravo": nil, // cyclic reference + }, + }, + } + v["Foo"].Bravos["FooBravo"].Alphas["Foo"] = v["Foo"] + v["Bar"].Bravos["BarBuzzBravo"].Alphas["Bar"] = v["Bar"] + v["Bar"].Bravos["BarBuzzBravo"].Alphas["Buzz"] = v["Buzz"] + v["Bar"].Bravos["BuzzBarBravo"].Alphas["Bar"] = v["Bar"] + v["Bar"].Bravos["BuzzBarBravo"].Alphas["Buzz"] = v["Buzz"] + v["Buzz"].Bravos["BarBuzzBravo"] = v["Bar"].Bravos["BarBuzzBravo"] + v["Buzz"].Bravos["BuzzBarBravo"] = v["Bar"].Bravos["BuzzBarBravo"] + return v + } + + var tests []test + type XY struct{ x, y interface{} } + for _, tt := range []struct { + label string + in XY + wantEqual bool + reason string + }{{ + label: "PointersEqual", + in: func() XY { + x := new(P) + *x = x + y := new(P) + *y = y + return XY{x, y} + }(), + wantEqual: true, + reason: "equal pair of single-node pointers", + }, { + label: "PointersInequal", + in: func() XY { + x := new(P) + *x = x + y1, y2 := new(P), new(P) + *y1 = y2 + *y2 = y1 + return XY{x, y1} + }(), + wantEqual: false, + reason: "inequal pair of single-node and double-node pointers", + }, { + label: "SlicesEqual", + in: func() XY { + x := S{nil} + x[0] = x + y := S{nil} + y[0] = y + return XY{x, y} + }(), + wantEqual: true, + reason: "equal pair of single-node slices", + }, { + label: "SlicesInequal", + in: func() XY { + x := S{nil} + x[0] = x + y1, y2 := S{nil}, S{nil} + y1[0] = y2 + y2[0] = y1 + return XY{x, y1} + }(), + wantEqual: false, + reason: "inequal pair of single-node and double node slices", + }, { + label: "MapsEqual", + in: func() XY { + x := M{0: nil} + x[0] = x + y := M{0: nil} + y[0] = y + return XY{x, y} + }(), + wantEqual: true, + reason: "equal pair of single-node maps", + }, { + label: "MapsInequal", + in: func() XY { + x := M{0: nil} + x[0] = x + y1, y2 := M{0: nil}, M{0: nil} + y1[0] = y2 + y2[0] = y1 + return XY{x, y1} + }(), + wantEqual: false, + reason: "inequal pair of single-node and double-node maps", + }, { + label: "GraphEqual", + in: XY{makeGraph(), makeGraph()}, + wantEqual: true, + reason: "graphs are equal since they have identical forms", + }, { + label: "GraphInequalZeroed", + in: func() XY { + x := makeGraph() + y := makeGraph() + y["Foo"].Bravos["FooBravo"].ID = 0 + y["Bar"].Bravos["BarBuzzBravo"].ID = 0 + y["Bar"].Bravos["BuzzBarBravo"].ID = 0 + return XY{x, y} + }(), + wantEqual: false, + reason: "graphs are inequal because the ID fields are different", + }, { + label: "GraphInequalStruct", + in: func() XY { + x := makeGraph() + y := makeGraph() + x["Buzz"].Bravos["BuzzBarBravo"] = &CycleBravo{ + Name: "BuzzBarBravo", + ID: 103, + } + return XY{x, y} + }(), + wantEqual: false, + reason: "graphs are inequal because they differ on a map element", + }} { + tests = append(tests, test{ + label: label + "/" + tt.label, + x: tt.in.x, + y: tt.in.y, + wantEqual: tt.wantEqual, + reason: tt.reason, + }) + } + return tests +} + +func project1Tests() []test { + const label = "Project1" + + ignoreUnexported := cmpopts.IgnoreUnexported( + ts.EagleImmutable{}, + ts.DreamerImmutable{}, + ts.SlapImmutable{}, + ts.GoatImmutable{}, + ts.DonkeyImmutable{}, + ts.LoveRadius{}, + ts.SummerLove{}, + ts.SummerLoveSummary{}, + ) + + createEagle := func() ts.Eagle { + return ts.Eagle{ + Name: "eagle", + Hounds: []string{"buford", "tannen"}, + Desc: "some description", + Dreamers: []ts.Dreamer{{}, { + Name: "dreamer2", + Animal: []interface{}{ + ts.Goat{ + Target: "corporation", + Immutable: &ts.GoatImmutable{ + ID: "southbay", + State: (*pb.Goat_States)(newInt(5)), + Started: now, + }, + }, + ts.Donkey{}, + }, + Amoeba: 53, + }}, + Slaps: []ts.Slap{{ + Name: "slapID", + Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, + Immutable: &ts.SlapImmutable{ + ID: "immutableSlap", + MildSlap: true, + Started: now, + LoveRadius: &ts.LoveRadius{ + Summer: &ts.SummerLove{ + Summary: &ts.SummerLoveSummary{ + Devices: []string{"foo", "bar", "baz"}, + ChangeType: []pb.SummerType{1, 2, 3}, + }, + }, + }, + }, + }}, + Immutable: &ts.EagleImmutable{ + ID: "eagleID", + Birthday: now, + MissingCall: (*pb.Eagle_MissingCalls)(newInt(55)), + }, + } + } + + return []test{{ + label: label + "/PanicUnexported", + x: ts.Eagle{Slaps: []ts.Slap{{ + Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, + }}}, + y: ts.Eagle{Slaps: []ts.Slap{{ + Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, + }}}, + wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", + }, { + label: label + "/ProtoEqual", + x: ts.Eagle{Slaps: []ts.Slap{{ + Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, + }}}, + y: ts.Eagle{Slaps: []ts.Slap{{ + Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, + }}}, + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + wantEqual: true, + reason: "simulated protobuf messages contain the same values", + }, { + label: label + "/ProtoInequal", + x: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { + Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, + }}}, + y: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { + Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata2"}}, + }}}, + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + wantEqual: false, + reason: "simulated protobuf messages contain different values", + }, { + label: label + "/Equal", + x: createEagle(), + y: createEagle(), + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantEqual: true, + reason: "equal because values are the same", + }, { + label: label + "/Inequal", + x: func() ts.Eagle { + eg := createEagle() + eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.ID = "southbay2" + eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.State = (*pb.Goat_States)(newInt(6)) + eg.Slaps[0].Immutable.MildSlap = false + return eg + }(), + y: func() ts.Eagle { + eg := createEagle() + devs := eg.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices + eg.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices = devs[:1] + return eg + }(), + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantEqual: false, + reason: "inequal because some values are different", + }} +} + +type germSorter []*pb.Germ + +func (gs germSorter) Len() int { return len(gs) } +func (gs germSorter) Less(i, j int) bool { return gs[i].String() < gs[j].String() } +func (gs germSorter) Swap(i, j int) { gs[i], gs[j] = gs[j], gs[i] } + +func project2Tests() []test { + const label = "Project2" + + sortGerms := cmp.Transformer("Sort", func(in []*pb.Germ) []*pb.Germ { + out := append([]*pb.Germ(nil), in...) // Make copy + sort.Sort(germSorter(out)) + return out + }) + + equalDish := cmp.Comparer(func(x, y *ts.Dish) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + px, err1 := x.Proto() + py, err2 := y.Proto() + if err1 != nil || err2 != nil { + return err1 == err2 + } + return pb.Equal(px, py) + }) + + createBatch := func() ts.GermBatch { + return ts.GermBatch{ + DirtyGerms: map[int32][]*pb.Germ{ + 17: { + {Stringer: pb.Stringer{X: "germ1"}}, + }, + 18: { + {Stringer: pb.Stringer{X: "germ2"}}, + {Stringer: pb.Stringer{X: "germ3"}}, + {Stringer: pb.Stringer{X: "germ4"}}, + }, + }, + GermMap: map[int32]*pb.Germ{ + 13: {Stringer: pb.Stringer{X: "germ13"}}, + 21: {Stringer: pb.Stringer{X: "germ21"}}, + }, + DishMap: map[int32]*ts.Dish{ + 0: ts.CreateDish(nil, io.EOF), + 1: ts.CreateDish(nil, io.ErrUnexpectedEOF), + 2: ts.CreateDish(&pb.Dish{Stringer: pb.Stringer{X: "dish"}}, nil), + }, + HasPreviousResult: true, + DirtyID: 10, + GermStrain: 421, + InfectedAt: now, + } + } + + return []test{{ + label: label + "/PanicUnexported", + x: createBatch(), + y: createBatch(), + wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", + }, { + label: label + "/Equal", + x: createBatch(), + y: createBatch(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: true, + reason: "equal because identical values are compared", + }, { + label: label + "/InequalOrder", + x: createBatch(), + y: func() ts.GermBatch { + gb := createBatch() + s := gb.DirtyGerms[18] + s[0], s[1], s[2] = s[1], s[2], s[0] + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, + wantEqual: false, + reason: "inequal because slice contains elements in differing order", + }, { + label: label + "/EqualOrder", + x: createBatch(), + y: func() ts.GermBatch { + gb := createBatch() + s := gb.DirtyGerms[18] + s[0], s[1], s[2] = s[1], s[2], s[0] + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: true, + reason: "equal because unordered slice is sorted using transformer", + }, { + label: label + "/Inequal", + x: func() ts.GermBatch { + gb := createBatch() + delete(gb.DirtyGerms, 17) + gb.DishMap[1] = nil + return gb + }(), + y: func() ts.GermBatch { + gb := createBatch() + gb.DirtyGerms[18] = gb.DirtyGerms[18][:2] + gb.GermStrain = 22 + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: false, + reason: "inequal because some values are different", + }} +} + +func project3Tests() []test { + const label = "Project3" + + allowVisibility := cmp.AllowUnexported(ts.Dirt{}) + + ignoreLocker := cmpopts.IgnoreInterfaces(struct{ sync.Locker }{}) + + transformProtos := cmp.Transformer("λ", func(x pb.Dirt) *pb.Dirt { + return &x + }) + + equalTable := cmp.Comparer(func(x, y ts.Table) bool { + tx, ok1 := x.(*ts.MockTable) + ty, ok2 := y.(*ts.MockTable) + if !ok1 || !ok2 { + panic("table type must be MockTable") + } + return cmp.Equal(tx.State(), ty.State()) + }) + + createDirt := func() (d ts.Dirt) { + d.SetTable(ts.CreateMockTable([]string{"a", "b", "c"})) + d.SetTimestamp(12345) + d.Discord = 554 + d.Proto = pb.Dirt{Stringer: pb.Stringer{X: "proto"}} + d.SetWizard(map[string]*pb.Wizard{ + "harry": {Stringer: pb.Stringer{X: "potter"}}, + "albus": {Stringer: pb.Stringer{X: "dumbledore"}}, + }) + d.SetLastTime(54321) + return d + } + + return []test{{ + label: label + "/PanicUnexported1", + x: createDirt(), + y: createDirt(), + wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", + }, { + label: label + "/PanicUnexported2", + x: createDirt(), + y: createDirt(), + opts: []cmp.Option{allowVisibility, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantPanic: "cannot handle unexported field", + reason: "struct contains references to simulated protobuf types with unexported fields", + }, { + label: label + "/Equal", + x: createDirt(), + y: createDirt(), + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantEqual: true, + reason: "transformer used to create reference to protobuf message so it works with pb.Equal", + }, { + label: label + "/Inequal", + x: func() ts.Dirt { + d := createDirt() + d.SetTable(ts.CreateMockTable([]string{"a", "c"})) + d.Proto = pb.Dirt{Stringer: pb.Stringer{X: "blah"}} + return d + }(), + y: func() ts.Dirt { + d := createDirt() + d.Discord = 500 + d.SetWizard(map[string]*pb.Wizard{ + "harry": {Stringer: pb.Stringer{X: "otter"}}, + }) + return d + }(), + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantEqual: false, + reason: "inequal because some values are different", + }} +} + +func project4Tests() []test { + const label = "Project4" + + allowVisibility := cmp.AllowUnexported( + ts.Cartel{}, + ts.Headquarter{}, + ts.Poison{}, + ) + + transformProtos := cmp.Transformer("λ", func(x pb.Restrictions) *pb.Restrictions { + return &x + }) + + createCartel := func() ts.Cartel { + var p ts.Poison + p.SetPoisonType(5) + p.SetExpiration(now) + p.SetManufacturer("acme") + + var hq ts.Headquarter + hq.SetID(5) + hq.SetLocation("moon") + hq.SetSubDivisions([]string{"alpha", "bravo", "charlie"}) + hq.SetMetaData(&pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}) + hq.SetPublicMessage([]byte{1, 2, 3, 4, 5}) + hq.SetHorseBack("abcdef") + hq.SetStatus(44) + + var c ts.Cartel + c.Headquarter = hq + c.SetSource("mars") + c.SetCreationTime(now) + c.SetBoss("al capone") + c.SetPoisons([]*ts.Poison{&p}) + + return c + } + + return []test{{ + label: label + "/PanicUnexported1", + x: createCartel(), + y: createCartel(), + wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", + }, { + label: label + "/PanicUnexported2", + x: createCartel(), + y: createCartel(), + opts: []cmp.Option{allowVisibility, cmp.Comparer(pb.Equal)}, + wantPanic: "cannot handle unexported field", + reason: "struct contains references to simulated protobuf types with unexported fields", + }, { + label: label + "/Equal", + x: createCartel(), + y: createCartel(), + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantEqual: true, + reason: "transformer used to create reference to protobuf message so it works with pb.Equal", + }, { + label: label + "/Inequal", + x: func() ts.Cartel { + d := createCartel() + var p1, p2 ts.Poison + p1.SetPoisonType(1) + p1.SetExpiration(now) + p1.SetManufacturer("acme") + p2.SetPoisonType(2) + p2.SetManufacturer("acme2") + d.SetPoisons([]*ts.Poison{&p1, &p2}) + return d + }(), + y: func() ts.Cartel { + d := createCartel() + d.SetSubDivisions([]string{"bravo", "charlie"}) + d.SetPublicMessage([]byte{1, 2, 4, 3, 5}) + return d + }(), + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantEqual: false, + reason: "inequal because some values are different", + }} +} + +// BenchmarkBytes benchmarks the performance of performing Equal or Diff on +// large slices of bytes. +func BenchmarkBytes(b *testing.B) { + // Create a list of PathFilters that never apply, but are evaluated. + const maxFilters = 5 + var filters cmp.Options + errorIface := reflect.TypeOf((*error)(nil)).Elem() + for i := 0; i <= maxFilters; i++ { + filters = append(filters, cmp.FilterPath(func(p cmp.Path) bool { + return p.Last().Type().AssignableTo(errorIface) // Never true + }, cmp.Ignore())) + } + + type benchSize struct { + label string + size int64 + } + for _, ts := range []benchSize{ + {"4KiB", 1 << 12}, + {"64KiB", 1 << 16}, + {"1MiB", 1 << 20}, + {"16MiB", 1 << 24}, + } { + bx := append(append(make([]byte, ts.size/2), 'x'), make([]byte, ts.size/2)...) + by := append(append(make([]byte, ts.size/2), 'y'), make([]byte, ts.size/2)...) + b.Run(ts.label, func(b *testing.B) { + // Iteratively add more filters that never apply, but are evaluated + // to measure the cost of simply evaluating each filter. + for i := 0; i <= maxFilters; i++ { + b.Run(fmt.Sprintf("EqualFilter%d", i), func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(2 * ts.size) + for j := 0; j < b.N; j++ { + cmp.Equal(bx, by, filters[:i]...) + } + }) + } + for i := 0; i <= maxFilters; i++ { + b.Run(fmt.Sprintf("DiffFilter%d", i), func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(2 * ts.size) + for j := 0; j < b.N; j++ { + cmp.Diff(bx, by, filters[:i]...) + } + }) + } + }) + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/example_reporter_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/example_reporter_test.go new file mode 100644 index 0000000..bacba28 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/example_reporter_test.go @@ -0,0 +1,59 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp_test + +import ( + "fmt" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// DiffReporter is a simple custom reporter that only records differences +// detected during comparison. +type DiffReporter struct { + path cmp.Path + diffs []string +} + +func (r *DiffReporter) PushStep(ps cmp.PathStep) { + r.path = append(r.path, ps) +} + +func (r *DiffReporter) Report(rs cmp.Result) { + if !rs.Equal() { + vx, vy := r.path.Last().Values() + r.diffs = append(r.diffs, fmt.Sprintf("%#v:\n\t-: %+v\n\t+: %+v\n", r.path, vx, vy)) + } +} + +func (r *DiffReporter) PopStep() { + r.path = r.path[:len(r.path)-1] +} + +func (r *DiffReporter) String() string { + return strings.Join(r.diffs, "\n") +} + +func ExampleReporter() { + x, y := MakeGatewayInfo() + + var r DiffReporter + cmp.Equal(x, y, cmp.Reporter(&r)) + fmt.Print(r.String()) + + // Output: + // {cmp_test.Gateway}.IPAddress: + // -: 192.168.0.1 + // +: 192.168.0.2 + // + // {cmp_test.Gateway}.Clients[4].IPAddress: + // -: 192.168.0.219 + // +: 192.168.0.221 + // + // {cmp_test.Gateway}.Clients[5->?]: + // -: {Hostname:americano IPAddress:192.168.0.188 LastSeen:2009-11-10 23:03:05 +0000 UTC} + // +: +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/example_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/example_test.go new file mode 100644 index 0000000..d165383 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/example_test.go @@ -0,0 +1,376 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp_test + +import ( + "fmt" + "math" + "net" + "reflect" + "sort" + "strings" + "time" + + "github.com/google/go-cmp/cmp" +) + +// TODO: Re-write these examples in terms of how you actually use the +// fundamental options and filters and not in terms of what cool things you can +// do with them since that overlaps with cmp/cmpopts. + +// Use Diff to print out a human-readable report of differences for tests +// comparing nested or structured data. +func ExampleDiff_testing() { + // Let got be the hypothetical value obtained from some logic under test + // and want be the expected golden data. + got, want := MakeGatewayInfo() + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff) + } + + // Output: + // MakeGatewayInfo() mismatch (-want +got): + // cmp_test.Gateway{ + // SSID: "CoffeeShopWiFi", + // - IPAddress: s"192.168.0.2", + // + IPAddress: s"192.168.0.1", + // NetMask: {0xff, 0xff, 0x00, 0x00}, + // Clients: []cmp_test.Client{ + // ... // 2 identical elements + // {Hostname: "macchiato", IPAddress: s"192.168.0.153", LastSeen: s"2009-11-10 23:39:43 +0000 UTC"}, + // {Hostname: "espresso", IPAddress: s"192.168.0.121"}, + // { + // Hostname: "latte", + // - IPAddress: s"192.168.0.221", + // + IPAddress: s"192.168.0.219", + // LastSeen: s"2009-11-10 23:00:23 +0000 UTC", + // }, + // + { + // + Hostname: "americano", + // + IPAddress: s"192.168.0.188", + // + LastSeen: s"2009-11-10 23:03:05 +0000 UTC", + // + }, + // }, + // } +} + +// Approximate equality for floats can be handled by defining a custom +// comparer on floats that determines two values to be equal if they are within +// some range of each other. +// +// This example is for demonstrative purposes; use cmpopts.EquateApprox instead. +func ExampleOption_approximateFloats() { + // This Comparer only operates on float64. + // To handle float32s, either define a similar function for that type + // or use a Transformer to convert float32s into float64s. + opt := cmp.Comparer(func(x, y float64) bool { + delta := math.Abs(x - y) + mean := math.Abs(x+y) / 2.0 + return delta/mean < 0.00001 + }) + + x := []float64{1.0, 1.1, 1.2, math.Pi} + y := []float64{1.0, 1.1, 1.2, 3.14159265359} // Accurate enough to Pi + z := []float64{1.0, 1.1, 1.2, 3.1415} // Diverges too far from Pi + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// Normal floating-point arithmetic defines == to be false when comparing +// NaN with itself. In certain cases, this is not the desired property. +// +// This example is for demonstrative purposes; use cmpopts.EquateNaNs instead. +func ExampleOption_equalNaNs() { + // This Comparer only operates on float64. + // To handle float32s, either define a similar function for that type + // or use a Transformer to convert float32s into float64s. + opt := cmp.Comparer(func(x, y float64) bool { + return (math.IsNaN(x) && math.IsNaN(y)) || x == y + }) + + x := []float64{1.0, math.NaN(), math.E, -0.0, +0.0} + y := []float64{1.0, math.NaN(), math.E, -0.0, +0.0} + z := []float64{1.0, math.NaN(), math.Pi, -0.0, +0.0} // Pi constant instead of E + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// To have floating-point comparisons combine both properties of NaN being +// equal to itself and also approximate equality of values, filters are needed +// to restrict the scope of the comparison so that they are composable. +// +// This example is for demonstrative purposes; +// use cmpopts.EquateNaNs and cmpopts.EquateApprox instead. +func ExampleOption_equalNaNsAndApproximateFloats() { + alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) + + opts := cmp.Options{ + // This option declares that a float64 comparison is equal only if + // both inputs are NaN. + cmp.FilterValues(func(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) + }, alwaysEqual), + + // This option declares approximate equality on float64s only if + // both inputs are not NaN. + cmp.FilterValues(func(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) + }, cmp.Comparer(func(x, y float64) bool { + delta := math.Abs(x - y) + mean := math.Abs(x+y) / 2.0 + return delta/mean < 0.00001 + })), + } + + x := []float64{math.NaN(), 1.0, 1.1, 1.2, math.Pi} + y := []float64{math.NaN(), 1.0, 1.1, 1.2, 3.14159265359} // Accurate enough to Pi + z := []float64{math.NaN(), 1.0, 1.1, 1.2, 3.1415} // Diverges too far from Pi + + fmt.Println(cmp.Equal(x, y, opts)) + fmt.Println(cmp.Equal(y, z, opts)) + fmt.Println(cmp.Equal(z, x, opts)) + + // Output: + // true + // false + // false +} + +// Sometimes, an empty map or slice is considered equal to an allocated one +// of zero length. +// +// This example is for demonstrative purposes; use cmpopts.EquateEmpty instead. +func ExampleOption_equalEmpty() { + alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) + + // This option handles slices and maps of any type. + opt := cmp.FilterValues(func(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (vx.IsValid() && vy.IsValid() && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) + }, alwaysEqual) + + type S struct { + A []int + B map[string]bool + } + x := S{nil, make(map[string]bool, 100)} + y := S{make([]int, 0, 200), nil} + z := S{[]int{0}, nil} // []int has a single element (i.e., not empty) + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// Two slices may be considered equal if they have the same elements, +// regardless of the order that they appear in. Transformations can be used +// to sort the slice. +// +// This example is for demonstrative purposes; use cmpopts.SortSlices instead. +func ExampleOption_sortedSlice() { + // This Transformer sorts a []int. + trans := cmp.Transformer("Sort", func(in []int) []int { + out := append([]int(nil), in...) // Copy input to avoid mutating it + sort.Ints(out) + return out + }) + + x := struct{ Ints []int }{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} + y := struct{ Ints []int }{[]int{2, 8, 0, 9, 6, 1, 4, 7, 3, 5}} + z := struct{ Ints []int }{[]int{0, 0, 1, 2, 3, 4, 5, 6, 7, 8}} + + fmt.Println(cmp.Equal(x, y, trans)) + fmt.Println(cmp.Equal(y, z, trans)) + fmt.Println(cmp.Equal(z, x, trans)) + + // Output: + // true + // false + // false +} + +type otherString string + +func (x otherString) Equal(y otherString) bool { + return strings.ToLower(string(x)) == strings.ToLower(string(y)) +} + +// If the Equal method defined on a type is not suitable, the type can be +// dynamically transformed to be stripped of the Equal method (or any method +// for that matter). +func ExampleOption_avoidEqualMethod() { + // Suppose otherString.Equal performs a case-insensitive equality, + // which is too loose for our needs. + // We can avoid the methods of otherString by declaring a new type. + type myString otherString + + // This transformer converts otherString to myString, allowing Equal to use + // other Options to determine equality. + trans := cmp.Transformer("", func(in otherString) myString { + return myString(in) + }) + + x := []otherString{"foo", "bar", "baz"} + y := []otherString{"fOO", "bAr", "Baz"} // Same as before, but with different case + + fmt.Println(cmp.Equal(x, y)) // Equal because of case-insensitivity + fmt.Println(cmp.Equal(x, y, trans)) // Not equal because of more exact equality + + // Output: + // true + // false +} + +func roundF64(z float64) float64 { + if z < 0 { + return math.Ceil(z - 0.5) + } + return math.Floor(z + 0.5) +} + +// The complex numbers complex64 and complex128 can really just be decomposed +// into a pair of float32 or float64 values. It would be convenient to be able +// define only a single comparator on float64 and have float32, complex64, and +// complex128 all be able to use that comparator. Transformations can be used +// to handle this. +func ExampleOption_transformComplex() { + opts := []cmp.Option{ + // This transformer decomposes complex128 into a pair of float64s. + cmp.Transformer("T1", func(in complex128) (out struct{ Real, Imag float64 }) { + out.Real, out.Imag = real(in), imag(in) + return out + }), + // This transformer converts complex64 to complex128 to allow the + // above transform to take effect. + cmp.Transformer("T2", func(in complex64) complex128 { + return complex128(in) + }), + // This transformer converts float32 to float64. + cmp.Transformer("T3", func(in float32) float64 { + return float64(in) + }), + // This equality function compares float64s as rounded integers. + cmp.Comparer(func(x, y float64) bool { + return roundF64(x) == roundF64(y) + }), + } + + x := []interface{}{ + complex128(3.0), complex64(5.1 + 2.9i), float32(-1.2), float64(12.3), + } + y := []interface{}{ + complex128(3.1), complex64(4.9 + 3.1i), float32(-1.3), float64(11.7), + } + z := []interface{}{ + complex128(3.8), complex64(4.9 + 3.1i), float32(-1.3), float64(11.7), + } + + fmt.Println(cmp.Equal(x, y, opts...)) + fmt.Println(cmp.Equal(y, z, opts...)) + fmt.Println(cmp.Equal(z, x, opts...)) + + // Output: + // true + // false + // false +} + +type ( + Gateway struct { + SSID string + IPAddress net.IP + NetMask net.IPMask + Clients []Client + } + Client struct { + Hostname string + IPAddress net.IP + LastSeen time.Time + } +) + +func MakeGatewayInfo() (x, y Gateway) { + x = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 1), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 219), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }, { + Hostname: "americano", + IPAddress: net.IPv4(192, 168, 0, 188), + LastSeen: time.Date(2009, time.November, 10, 23, 3, 5, 0, time.UTC), + }}, + } + y = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 2), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 221), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }}, + } + return x, y +} + +var t fakeT + +type fakeT struct{} + +func (t fakeT) Errorf(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) } diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/export_panic.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/export_panic.go new file mode 100644 index 0000000..5ff0b42 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/export_panic.go @@ -0,0 +1,15 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build purego + +package cmp + +import "reflect" + +const supportExporters = false + +func retrieveUnexportedField(reflect.Value, reflect.StructField, bool) reflect.Value { + panic("no support for forcibly accessing unexported fields") +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/export_unsafe.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/export_unsafe.go new file mode 100644 index 0000000..21eb548 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/export_unsafe.go @@ -0,0 +1,35 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !purego + +package cmp + +import ( + "reflect" + "unsafe" +) + +const supportExporters = true + +// retrieveUnexportedField uses unsafe to forcibly retrieve any field from +// a struct such that the value has read-write permissions. +// +// The parent struct, v, must be addressable, while f must be a StructField +// describing the field to retrieve. If addr is false, +// then the returned value will be shallowed copied to be non-addressable. +func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) reflect.Value { + ve := reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem() + if !addr { + // A field is addressable if and only if the struct is addressable. + // If the original parent value was not addressable, shallow copy the + // value to make it non-addressable to avoid leaking an implementation + // detail of how forcibly exporting a field works. + if ve.Kind() == reflect.Interface && ve.IsNil() { + return reflect.Zero(f.Type) + } + return reflect.ValueOf(ve.Interface()).Convert(f.Type) + } + return ve +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/debug_disable.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/debug_disable.go new file mode 100644 index 0000000..1daaaac --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/debug_disable.go @@ -0,0 +1,17 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !cmp_debug + +package diff + +var debug debugger + +type debugger struct{} + +func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc { + return f +} +func (debugger) Update() {} +func (debugger) Finish() {} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/debug_enable.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/debug_enable.go new file mode 100644 index 0000000..4b91dbc --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/debug_enable.go @@ -0,0 +1,122 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build cmp_debug + +package diff + +import ( + "fmt" + "strings" + "sync" + "time" +) + +// The algorithm can be seen running in real-time by enabling debugging: +// go test -tags=cmp_debug -v +// +// Example output: +// === RUN TestDifference/#34 +// ┌───────────────────────────────┐ +// │ \ · · · · · · · · · · · · · · │ +// │ · # · · · · · · · · · · · · · │ +// │ · \ · · · · · · · · · · · · · │ +// │ · · \ · · · · · · · · · · · · │ +// │ · · · X # · · · · · · · · · · │ +// │ · · · # \ · · · · · · · · · · │ +// │ · · · · · # # · · · · · · · · │ +// │ · · · · · # \ · · · · · · · · │ +// │ · · · · · · · \ · · · · · · · │ +// │ · · · · · · · · \ · · · · · · │ +// │ · · · · · · · · · \ · · · · · │ +// │ · · · · · · · · · · \ · · # · │ +// │ · · · · · · · · · · · \ # # · │ +// │ · · · · · · · · · · · # # # · │ +// │ · · · · · · · · · · # # # # · │ +// │ · · · · · · · · · # # # # # · │ +// │ · · · · · · · · · · · · · · \ │ +// └───────────────────────────────┘ +// [.Y..M.XY......YXYXY.|] +// +// The grid represents the edit-graph where the horizontal axis represents +// list X and the vertical axis represents list Y. The start of the two lists +// is the top-left, while the ends are the bottom-right. The '·' represents +// an unexplored node in the graph. The '\' indicates that the two symbols +// from list X and Y are equal. The 'X' indicates that two symbols are similar +// (but not exactly equal) to each other. The '#' indicates that the two symbols +// are different (and not similar). The algorithm traverses this graph trying to +// make the paths starting in the top-left and the bottom-right connect. +// +// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents +// the currently established path from the forward and reverse searches, +// separated by a '|' character. + +const ( + updateDelay = 100 * time.Millisecond + finishDelay = 500 * time.Millisecond + ansiTerminal = true // ANSI escape codes used to move terminal cursor +) + +var debug debugger + +type debugger struct { + sync.Mutex + p1, p2 EditScript + fwdPath, revPath *EditScript + grid []byte + lines int +} + +func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc { + dbg.Lock() + dbg.fwdPath, dbg.revPath = p1, p2 + top := "┌─" + strings.Repeat("──", nx) + "┐\n" + row := "│ " + strings.Repeat("· ", nx) + "│\n" + btm := "└─" + strings.Repeat("──", nx) + "┘\n" + dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) + dbg.lines = strings.Count(dbg.String(), "\n") + fmt.Print(dbg) + + // Wrap the EqualFunc so that we can intercept each result. + return func(ix, iy int) (r Result) { + cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")] + for i := range cell { + cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot + } + switch r = f(ix, iy); { + case r.Equal(): + cell[0] = '\\' + case r.Similar(): + cell[0] = 'X' + default: + cell[0] = '#' + } + return + } +} + +func (dbg *debugger) Update() { + dbg.print(updateDelay) +} + +func (dbg *debugger) Finish() { + dbg.print(finishDelay) + dbg.Unlock() +} + +func (dbg *debugger) String() string { + dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] + for i := len(*dbg.revPath) - 1; i >= 0; i-- { + dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) + } + return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2) +} + +func (dbg *debugger) print(d time.Duration) { + if ansiTerminal { + fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor + } + fmt.Print(dbg) + time.Sleep(d) +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/diff.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/diff.go new file mode 100644 index 0000000..bc196b1 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/diff.go @@ -0,0 +1,398 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package diff implements an algorithm for producing edit-scripts. +// The edit-script is a sequence of operations needed to transform one list +// of symbols into another (or vice-versa). The edits allowed are insertions, +// deletions, and modifications. The summation of all edits is called the +// Levenshtein distance as this problem is well-known in computer science. +// +// This package prioritizes performance over accuracy. That is, the run time +// is more important than obtaining a minimal Levenshtein distance. +package diff + +import ( + "math/rand" + "time" + + "github.com/google/go-cmp/cmp/internal/flags" +) + +// EditType represents a single operation within an edit-script. +type EditType uint8 + +const ( + // Identity indicates that a symbol pair is identical in both list X and Y. + Identity EditType = iota + // UniqueX indicates that a symbol only exists in X and not Y. + UniqueX + // UniqueY indicates that a symbol only exists in Y and not X. + UniqueY + // Modified indicates that a symbol pair is a modification of each other. + Modified +) + +// EditScript represents the series of differences between two lists. +type EditScript []EditType + +// String returns a human-readable string representing the edit-script where +// Identity, UniqueX, UniqueY, and Modified are represented by the +// '.', 'X', 'Y', and 'M' characters, respectively. +func (es EditScript) String() string { + b := make([]byte, len(es)) + for i, e := range es { + switch e { + case Identity: + b[i] = '.' + case UniqueX: + b[i] = 'X' + case UniqueY: + b[i] = 'Y' + case Modified: + b[i] = 'M' + default: + panic("invalid edit-type") + } + } + return string(b) +} + +// stats returns a histogram of the number of each type of edit operation. +func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) { + for _, e := range es { + switch e { + case Identity: + s.NI++ + case UniqueX: + s.NX++ + case UniqueY: + s.NY++ + case Modified: + s.NM++ + default: + panic("invalid edit-type") + } + } + return +} + +// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if +// lists X and Y are equal. +func (es EditScript) Dist() int { return len(es) - es.stats().NI } + +// LenX is the length of the X list. +func (es EditScript) LenX() int { return len(es) - es.stats().NY } + +// LenY is the length of the Y list. +func (es EditScript) LenY() int { return len(es) - es.stats().NX } + +// EqualFunc reports whether the symbols at indexes ix and iy are equal. +// When called by Difference, the index is guaranteed to be within nx and ny. +type EqualFunc func(ix int, iy int) Result + +// Result is the result of comparison. +// NumSame is the number of sub-elements that are equal. +// NumDiff is the number of sub-elements that are not equal. +type Result struct{ NumSame, NumDiff int } + +// BoolResult returns a Result that is either Equal or not Equal. +func BoolResult(b bool) Result { + if b { + return Result{NumSame: 1} // Equal, Similar + } else { + return Result{NumDiff: 2} // Not Equal, not Similar + } +} + +// Equal indicates whether the symbols are equal. Two symbols are equal +// if and only if NumDiff == 0. If Equal, then they are also Similar. +func (r Result) Equal() bool { return r.NumDiff == 0 } + +// Similar indicates whether two symbols are similar and may be represented +// by using the Modified type. As a special case, we consider binary comparisons +// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. +// +// The exact ratio of NumSame to NumDiff to determine similarity may change. +func (r Result) Similar() bool { + // Use NumSame+1 to offset NumSame so that binary comparisons are similar. + return r.NumSame+1 >= r.NumDiff +} + +var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 + +// Difference reports whether two lists of lengths nx and ny are equal +// given the definition of equality provided as f. +// +// This function returns an edit-script, which is a sequence of operations +// needed to convert one list into the other. The following invariants for +// the edit-script are maintained: +// • eq == (es.Dist()==0) +// • nx == es.LenX() +// • ny == es.LenY() +// +// This algorithm is not guaranteed to be an optimal solution (i.e., one that +// produces an edit-script with a minimal Levenshtein distance). This algorithm +// favors performance over optimality. The exact output is not guaranteed to +// be stable and may change over time. +func Difference(nx, ny int, f EqualFunc) (es EditScript) { + // This algorithm is based on traversing what is known as an "edit-graph". + // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" + // by Eugene W. Myers. Since D can be as large as N itself, this is + // effectively O(N^2). Unlike the algorithm from that paper, we are not + // interested in the optimal path, but at least some "decent" path. + // + // For example, let X and Y be lists of symbols: + // X = [A B C A B B A] + // Y = [C B A B A C] + // + // The edit-graph can be drawn as the following: + // A B C A B B A + // ┌─────────────┐ + // C │_|_|\|_|_|_|_│ 0 + // B │_|\|_|_|\|\|_│ 1 + // A │\|_|_|\|_|_|\│ 2 + // B │_|\|_|_|\|\|_│ 3 + // A │\|_|_|\|_|_|\│ 4 + // C │ | |\| | | | │ 5 + // └─────────────┘ 6 + // 0 1 2 3 4 5 6 7 + // + // List X is written along the horizontal axis, while list Y is written + // along the vertical axis. At any point on this grid, if the symbol in + // list X matches the corresponding symbol in list Y, then a '\' is drawn. + // The goal of any minimal edit-script algorithm is to find a path from the + // top-left corner to the bottom-right corner, while traveling through the + // fewest horizontal or vertical edges. + // A horizontal edge is equivalent to inserting a symbol from list X. + // A vertical edge is equivalent to inserting a symbol from list Y. + // A diagonal edge is equivalent to a matching symbol between both X and Y. + + // Invariants: + // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx + // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny + // + // In general: + // • fwdFrontier.X < revFrontier.X + // • fwdFrontier.Y < revFrontier.Y + // Unless, it is time for the algorithm to terminate. + fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} + revPath := path{-1, point{nx, ny}, make(EditScript, 0)} + fwdFrontier := fwdPath.point // Forward search frontier + revFrontier := revPath.point // Reverse search frontier + + // Search budget bounds the cost of searching for better paths. + // The longest sequence of non-matching symbols that can be tolerated is + // approximately the square-root of the search budget. + searchBudget := 4 * (nx + ny) // O(n) + + // Running the tests with the "cmp_debug" build tag prints a visualization + // of the algorithm running in real-time. This is educational for + // understanding how the algorithm works. See debug_enable.go. + f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) + + // The algorithm below is a greedy, meet-in-the-middle algorithm for + // computing sub-optimal edit-scripts between two lists. + // + // The algorithm is approximately as follows: + // • Searching for differences switches back-and-forth between + // a search that starts at the beginning (the top-left corner), and + // a search that starts at the end (the bottom-right corner). The goal of + // the search is connect with the search from the opposite corner. + // • As we search, we build a path in a greedy manner, where the first + // match seen is added to the path (this is sub-optimal, but provides a + // decent result in practice). When matches are found, we try the next pair + // of symbols in the lists and follow all matches as far as possible. + // • When searching for matches, we search along a diagonal going through + // through the "frontier" point. If no matches are found, we advance the + // frontier towards the opposite corner. + // • This algorithm terminates when either the X coordinates or the + // Y coordinates of the forward and reverse frontier points ever intersect. + + // This algorithm is correct even if searching only in the forward direction + // or in the reverse direction. We do both because it is commonly observed + // that two lists commonly differ because elements were added to the front + // or end of the other list. + // + // Non-deterministically start with either the forward or reverse direction + // to introduce some deliberate instability so that we have the flexibility + // to change this algorithm in the future. + if flags.Deterministic || randBool { + goto forwardSearch + } else { + goto reverseSearch + } + +forwardSearch: + { + // Forward search from the beginning. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + goto finishSearch + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{fwdFrontier.X + z, fwdFrontier.Y - z} + switch { + case p.X >= revPath.X || p.Y < fwdPath.Y: + stop1 = true // Hit top-right corner + case p.Y >= revPath.Y || p.X < fwdPath.X: + stop2 = true // Hit bottom-left corner + case f(p.X, p.Y).Equal(): + // Match found, so connect the path to this point. + fwdPath.connect(p, f) + fwdPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(fwdPath.X, fwdPath.Y).Equal() { + break + } + fwdPath.append(Identity) + } + fwdFrontier = fwdPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards reverse point. + if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y { + fwdFrontier.X++ + } else { + fwdFrontier.Y++ + } + goto reverseSearch + } + +reverseSearch: + { + // Reverse search from the end. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + goto finishSearch + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{revFrontier.X - z, revFrontier.Y + z} + switch { + case fwdPath.X >= p.X || revPath.Y < p.Y: + stop1 = true // Hit bottom-left corner + case fwdPath.Y >= p.Y || revPath.X < p.X: + stop2 = true // Hit top-right corner + case f(p.X-1, p.Y-1).Equal(): + // Match found, so connect the path to this point. + revPath.connect(p, f) + revPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(revPath.X-1, revPath.Y-1).Equal() { + break + } + revPath.append(Identity) + } + revFrontier = revPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards forward point. + if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y { + revFrontier.X-- + } else { + revFrontier.Y-- + } + goto forwardSearch + } + +finishSearch: + // Join the forward and reverse paths and then append the reverse path. + fwdPath.connect(revPath.point, f) + for i := len(revPath.es) - 1; i >= 0; i-- { + t := revPath.es[i] + revPath.es = revPath.es[:i] + fwdPath.append(t) + } + debug.Finish() + return fwdPath.es +} + +type path struct { + dir int // +1 if forward, -1 if reverse + point // Leading point of the EditScript path + es EditScript +} + +// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types +// to the edit-script to connect p.point to dst. +func (p *path) connect(dst point, f EqualFunc) { + if p.dir > 0 { + // Connect in forward direction. + for dst.X > p.X && dst.Y > p.Y { + switch r := f(p.X, p.Y); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case dst.X-p.X >= dst.Y-p.Y: + p.append(UniqueX) + default: + p.append(UniqueY) + } + } + for dst.X > p.X { + p.append(UniqueX) + } + for dst.Y > p.Y { + p.append(UniqueY) + } + } else { + // Connect in reverse direction. + for p.X > dst.X && p.Y > dst.Y { + switch r := f(p.X-1, p.Y-1); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case p.Y-dst.Y >= p.X-dst.X: + p.append(UniqueY) + default: + p.append(UniqueX) + } + } + for p.X > dst.X { + p.append(UniqueX) + } + for p.Y > dst.Y { + p.append(UniqueY) + } + } +} + +func (p *path) append(t EditType) { + p.es = append(p.es, t) + switch t { + case Identity, Modified: + p.add(p.dir, p.dir) + case UniqueX: + p.add(p.dir, 0) + case UniqueY: + p.add(0, p.dir) + } + debug.Update() +} + +type point struct{ X, Y int } + +func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } + +// zigzag maps a consecutive sequence of integers to a zig-zag sequence. +// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] +func zigzag(x int) int { + if x&1 != 0 { + x = ^x + } + return x >> 1 +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/diff_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/diff_test.go new file mode 100644 index 0000000..eacf072 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/diff/diff_test.go @@ -0,0 +1,449 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diff + +import ( + "fmt" + "math/rand" + "strings" + "testing" + "unicode" +) + +func TestDifference(t *testing.T) { + tests := []struct { + // Before passing x and y to Difference, we strip all spaces so that + // they can be used by the test author to indicate a missing symbol + // in one of the lists. + x, y string + want string // '|' separated list of possible outputs + }{{ + x: "", + y: "", + want: "", + }, { + x: "#", + y: "#", + want: ".", + }, { + x: "##", + y: "# ", + want: ".X|X.", + }, { + x: "a#", + y: "A ", + want: "MX", + }, { + x: "#a", + y: " A", + want: "XM", + }, { + x: "# ", + y: "##", + want: ".Y|Y.", + }, { + x: " #", + y: "@#", + want: "Y.", + }, { + x: "@#", + y: " #", + want: "X.", + }, { + x: "##########0123456789", + y: " 0123456789", + want: "XXXXXXXXXX..........", + }, { + x: " 0123456789", + y: "##########0123456789", + want: "YYYYYYYYYY..........", + }, { + x: "#####0123456789#####", + y: " 0123456789 ", + want: "XXXXX..........XXXXX", + }, { + x: " 0123456789 ", + y: "#####0123456789#####", + want: "YYYYY..........YYYYY", + }, { + x: "01234##########56789", + y: "01234 56789", + want: ".....XXXXXXXXXX.....", + }, { + x: "01234 56789", + y: "01234##########56789", + want: ".....YYYYYYYYYY.....", + }, { + x: "0123456789##########", + y: "0123456789 ", + want: "..........XXXXXXXXXX", + }, { + x: "0123456789 ", + y: "0123456789##########", + want: "..........YYYYYYYYYY", + }, { + x: "abcdefghij0123456789", + y: "ABCDEFGHIJ0123456789", + want: "MMMMMMMMMM..........", + }, { + x: "ABCDEFGHIJ0123456789", + y: "abcdefghij0123456789", + want: "MMMMMMMMMM..........", + }, { + x: "01234abcdefghij56789", + y: "01234ABCDEFGHIJ56789", + want: ".....MMMMMMMMMM.....", + }, { + x: "01234ABCDEFGHIJ56789", + y: "01234abcdefghij56789", + want: ".....MMMMMMMMMM.....", + }, { + x: "0123456789abcdefghij", + y: "0123456789ABCDEFGHIJ", + want: "..........MMMMMMMMMM", + }, { + x: "0123456789ABCDEFGHIJ", + y: "0123456789abcdefghij", + want: "..........MMMMMMMMMM", + }, { + x: "ABCDEFGHIJ0123456789 ", + y: " 0123456789abcdefghij", + want: "XXXXXXXXXX..........YYYYYYYYYY", + }, { + x: " 0123456789abcdefghij", + y: "ABCDEFGHIJ0123456789 ", + want: "YYYYYYYYYY..........XXXXXXXXXX", + }, { + x: "ABCDE0123456789 FGHIJ", + y: " 0123456789abcdefghij", + want: "XXXXX..........YYYYYMMMMM", + }, { + x: " 0123456789abcdefghij", + y: "ABCDE0123456789 FGHIJ", + want: "YYYYY..........XXXXXMMMMM", + }, { + x: "ABCDE01234F G H I J 56789 ", + y: " 01234 a b c d e56789fghij", + want: "XXXXX.....XYXYXYXYXY.....YYYYY", + }, { + x: " 01234a b c d e 56789fghij", + y: "ABCDE01234 F G H I J56789 ", + want: "YYYYY.....XYXYXYXYXY.....XXXXX", + }, { + x: "FGHIJ01234ABCDE56789 ", + y: " 01234abcde56789fghij", + want: "XXXXX.....MMMMM.....YYYYY", + }, { + x: " 01234abcde56789fghij", + y: "FGHIJ01234ABCDE56789 ", + want: "YYYYY.....MMMMM.....XXXXX", + }, { + x: "ABCAB BA ", + y: " C BABAC", + want: "XX.X.Y..Y|XX.Y.X..Y", + }, { + x: "# #### ###", + y: "#y####yy###", + want: ".Y....YY...", + }, { + x: "# #### # ##x#x", + y: "#y####y y## # ", + want: ".Y....YXY..X.X", + }, { + x: "###z#z###### x #", + y: "#y##Z#Z###### yy#", + want: ".Y..M.M......XYY.", + }, { + x: "0 12z3x 456789 x x 0", + y: "0y12Z3 y456789y y y0", + want: ".Y..M.XY......YXYXY.|.Y..M.XY......XYXYY.", + }, { + x: "0 2 4 6 8 ..................abXXcdEXF.ghXi", + y: " 1 3 5 7 9..................AB CDE F.GH I", + want: "XYXYXYXYXY..................MMXXMM.X..MMXM", + }, { + x: "I HG.F EDC BA..................9 7 5 3 1 ", + y: "iXhg.FXEdcXXba.................. 8 6 4 2 0", + want: "MYMM..Y.MMYYMM..................XYXYXYXYXY", + }, { + x: "x1234", + y: " 1234", + want: "X....", + }, { + x: "x123x4", + y: " 123 4", + want: "X...X.", + }, { + x: "x1234x56", + y: " 1234 ", + want: "X....XXX", + }, { + x: "x1234xxx56", + y: " 1234 56", + want: "X....XXX..", + }, { + x: ".1234...ab", + y: " 1234 AB", + want: "X....XXXMM", + }, { + x: "x1234xxab.", + y: " 1234 AB ", + want: "X....XXMMX", + }, { + x: " 0123456789", + y: "9012345678 ", + want: "Y.........X", + }, { + x: " 0123456789", + y: "8901234567 ", + want: "YY........XX", + }, { + x: " 0123456789", + y: "7890123456 ", + want: "YYY.......XXX", + }, { + x: " 0123456789", + y: "6789012345 ", + want: "YYYY......XXXX", + }, { + x: "0123456789 ", + y: " 5678901234", + want: "XXXXX.....YYYYY|YYYYY.....XXXXX", + }, { + x: "0123456789 ", + y: " 4567890123", + want: "XXXX......YYYY", + }, { + x: "0123456789 ", + y: " 3456789012", + want: "XXX.......YYY", + }, { + x: "0123456789 ", + y: " 2345678901", + want: "XX........YY", + }, { + x: "0123456789 ", + y: " 1234567890", + want: "X.........Y", + }, { + x: "0 1 2 3 45 6 7 8 9 ", + y: " 9 8 7 6 54 3 2 1 0", + want: "XYXYXYXYX.YXYXYXYXY", + }, { + x: "0 1 2345678 9 ", + y: " 6 72 5 819034", + want: "XYXY.XX.XX.Y.YYY", + }, { + x: "F B Q M O I G T L N72X90 E 4S P 651HKRJU DA 83CVZW", + y: " 5 W H XO10R9IV K ZLCTAJ8P3N SEQM4 7 2G6 UBD F ", + want: "XYXYXYXY.YYYY.YXYXY.YYYYYYY.XXXXXY.YY.XYXYY.XXXXXX.Y.XYXXXXXX", + }} + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + x := strings.Replace(tt.x, " ", "", -1) + y := strings.Replace(tt.y, " ", "", -1) + es := testStrings(t, x, y) + var want string + got := es.String() + for _, want = range strings.Split(tt.want, "|") { + if got == want { + return + } + } + t.Errorf("Difference(%s, %s):\ngot %s\nwant %s", x, y, got, want) + }) + } +} + +func TestDifferenceFuzz(t *testing.T) { + tests := []struct{ px, py, pm float32 }{ + {px: 0.0, py: 0.0, pm: 0.1}, + {px: 0.0, py: 0.1, pm: 0.0}, + {px: 0.1, py: 0.0, pm: 0.0}, + {px: 0.0, py: 0.1, pm: 0.1}, + {px: 0.1, py: 0.0, pm: 0.1}, + {px: 0.2, py: 0.2, pm: 0.2}, + {px: 0.3, py: 0.1, pm: 0.2}, + {px: 0.1, py: 0.3, pm: 0.2}, + {px: 0.2, py: 0.2, pm: 0.2}, + {px: 0.3, py: 0.3, pm: 0.3}, + {px: 0.1, py: 0.1, pm: 0.5}, + {px: 0.4, py: 0.1, pm: 0.5}, + {px: 0.3, py: 0.2, pm: 0.5}, + {px: 0.2, py: 0.3, pm: 0.5}, + {px: 0.1, py: 0.4, pm: 0.5}, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("P%d", i), func(t *testing.T) { + // Sweep from 1B to 1KiB. + for n := 1; n <= 1024; n <<= 1 { + t.Run(fmt.Sprintf("N%d", n), func(t *testing.T) { + for j := 0; j < 10; j++ { + x, y := generateStrings(n, tt.px, tt.py, tt.pm, int64(j)) + testStrings(t, x, y) + } + }) + } + }) + } +} + +func BenchmarkDifference(b *testing.B) { + for n := 1 << 10; n <= 1<<20; n <<= 2 { + b.Run(fmt.Sprintf("N%d", n), func(b *testing.B) { + x, y := generateStrings(n, 0.05, 0.05, 0.10, 0) + b.ReportAllocs() + b.SetBytes(int64(len(x) + len(y))) + for i := 0; i < b.N; i++ { + Difference(len(x), len(y), func(ix, iy int) Result { + return compareByte(x[ix], y[iy]) + }) + } + }) + } +} + +func generateStrings(n int, px, py, pm float32, seed int64) (string, string) { + if px+py+pm > 1.0 { + panic("invalid probabilities") + } + py += px + pm += py + + b := make([]byte, n) + r := rand.New(rand.NewSource(seed)) + r.Read(b) + + var x, y []byte + for len(b) > 0 { + switch p := r.Float32(); { + case p < px: // UniqueX + x = append(x, b[0]) + case p < py: // UniqueY + y = append(y, b[0]) + case p < pm: // Modified + x = append(x, 'A'+(b[0]%26)) + y = append(y, 'a'+(b[0]%26)) + default: // Identity + x = append(x, b[0]) + y = append(y, b[0]) + } + b = b[1:] + } + return string(x), string(y) +} + +func testStrings(t *testing.T, x, y string) EditScript { + es := Difference(len(x), len(y), func(ix, iy int) Result { + return compareByte(x[ix], y[iy]) + }) + if es.LenX() != len(x) { + t.Errorf("es.LenX = %d, want %d", es.LenX(), len(x)) + } + if es.LenY() != len(y) { + t.Errorf("es.LenY = %d, want %d", es.LenY(), len(y)) + } + if !validateScript(x, y, es) { + t.Errorf("invalid edit script: %v", es) + } + return es +} + +func validateScript(x, y string, es EditScript) bool { + var bx, by []byte + for _, e := range es { + switch e { + case Identity: + if !compareByte(x[len(bx)], y[len(by)]).Equal() { + return false + } + bx = append(bx, x[len(bx)]) + by = append(by, y[len(by)]) + case UniqueX: + bx = append(bx, x[len(bx)]) + case UniqueY: + by = append(by, y[len(by)]) + case Modified: + if !compareByte(x[len(bx)], y[len(by)]).Similar() { + return false + } + bx = append(bx, x[len(bx)]) + by = append(by, y[len(by)]) + } + } + return string(bx) == x && string(by) == y +} + +// compareByte returns a Result where the result is Equal if x == y, +// similar if x and y differ only in casing, and different otherwise. +func compareByte(x, y byte) (r Result) { + switch { + case x == y: + return equalResult // Identity + case unicode.ToUpper(rune(x)) == unicode.ToUpper(rune(y)): + return similarResult // Modified + default: + return differentResult // UniqueX or UniqueY + } +} + +var ( + equalResult = Result{NumDiff: 0} + similarResult = Result{NumDiff: 1} + differentResult = Result{NumDiff: 2} +) + +func TestResult(t *testing.T) { + tests := []struct { + result Result + wantEqual bool + wantSimilar bool + }{ + // equalResult is equal since NumDiff == 0, by definition of Equal method. + {equalResult, true, true}, + // similarResult is similar since it is a binary result where only one + // element was compared (i.e., Either NumSame==1 or NumDiff==1). + {similarResult, false, true}, + // differentResult is different since there are enough differences that + // it isn't even considered similar. + {differentResult, false, false}, + + // Zero value is always equal. + {Result{NumSame: 0, NumDiff: 0}, true, true}, + + // Binary comparisons (where NumSame+NumDiff == 1) are always similar. + {Result{NumSame: 1, NumDiff: 0}, true, true}, + {Result{NumSame: 0, NumDiff: 1}, false, true}, + + // More complex ratios. The exact ratio for similarity may change, + // and may require updates to these test cases. + {Result{NumSame: 1, NumDiff: 1}, false, true}, + {Result{NumSame: 1, NumDiff: 2}, false, true}, + {Result{NumSame: 1, NumDiff: 3}, false, false}, + {Result{NumSame: 2, NumDiff: 1}, false, true}, + {Result{NumSame: 2, NumDiff: 2}, false, true}, + {Result{NumSame: 2, NumDiff: 3}, false, true}, + {Result{NumSame: 3, NumDiff: 1}, false, true}, + {Result{NumSame: 3, NumDiff: 2}, false, true}, + {Result{NumSame: 3, NumDiff: 3}, false, true}, + {Result{NumSame: 1000, NumDiff: 0}, true, true}, + {Result{NumSame: 1000, NumDiff: 1}, false, true}, + {Result{NumSame: 1000, NumDiff: 2}, false, true}, + {Result{NumSame: 0, NumDiff: 1000}, false, false}, + {Result{NumSame: 1, NumDiff: 1000}, false, false}, + {Result{NumSame: 2, NumDiff: 1000}, false, false}, + } + + for _, tt := range tests { + if got := tt.result.Equal(); got != tt.wantEqual { + t.Errorf("%#v.Equal() = %v, want %v", tt.result, got, tt.wantEqual) + } + if got := tt.result.Similar(); got != tt.wantSimilar { + t.Errorf("%#v.Similar() = %v, want %v", tt.result, got, tt.wantSimilar) + } + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/flags.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/flags.go new file mode 100644 index 0000000..d8e459c --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/flags.go @@ -0,0 +1,9 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package flags + +// Deterministic controls whether the output of Diff should be deterministic. +// This is only used for testing. +var Deterministic bool diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/toolchain_legacy.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/toolchain_legacy.go new file mode 100644 index 0000000..82d1d7f --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/toolchain_legacy.go @@ -0,0 +1,10 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.10 + +package flags + +// AtLeastGo110 reports whether the Go toolchain is at least Go 1.10. +const AtLeastGo110 = false diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/toolchain_recent.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/toolchain_recent.go new file mode 100644 index 0000000..8646f05 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/flags/toolchain_recent.go @@ -0,0 +1,10 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.10 + +package flags + +// AtLeastGo110 reports whether the Go toolchain is at least Go 1.10. +const AtLeastGo110 = true diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/function/func.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/function/func.go new file mode 100644 index 0000000..d127d43 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/function/func.go @@ -0,0 +1,99 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package function provides functionality for identifying function types. +package function + +import ( + "reflect" + "regexp" + "runtime" + "strings" +) + +type funcType int + +const ( + _ funcType = iota + + tbFunc // func(T) bool + ttbFunc // func(T, T) bool + trbFunc // func(T, R) bool + tibFunc // func(T, I) bool + trFunc // func(T) R + + Equal = ttbFunc // func(T, T) bool + EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool + Transformer = trFunc // func(T) R + ValueFilter = ttbFunc // func(T, T) bool + Less = ttbFunc // func(T, T) bool + ValuePredicate = tbFunc // func(T) bool + KeyValuePredicate = trbFunc // func(T, R) bool +) + +var boolType = reflect.TypeOf(true) + +// IsType reports whether the reflect.Type is of the specified function type. +func IsType(t reflect.Type, ft funcType) bool { + if t == nil || t.Kind() != reflect.Func || t.IsVariadic() { + return false + } + ni, no := t.NumIn(), t.NumOut() + switch ft { + case tbFunc: // func(T) bool + if ni == 1 && no == 1 && t.Out(0) == boolType { + return true + } + case ttbFunc: // func(T, T) bool + if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { + return true + } + case trbFunc: // func(T, R) bool + if ni == 2 && no == 1 && t.Out(0) == boolType { + return true + } + case tibFunc: // func(T, I) bool + if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { + return true + } + case trFunc: // func(T) R + if ni == 1 && no == 1 { + return true + } + } + return false +} + +var lastIdentRx = regexp.MustCompile(`[_\p{L}][_\p{L}\p{N}]*$`) + +// NameOf returns the name of the function value. +func NameOf(v reflect.Value) string { + fnc := runtime.FuncForPC(v.Pointer()) + if fnc == nil { + return "" + } + fullName := fnc.Name() // e.g., "long/path/name/mypkg.(*MyType).(long/path/name/mypkg.myMethod)-fm" + + // Method closures have a "-fm" suffix. + fullName = strings.TrimSuffix(fullName, "-fm") + + var name string + for len(fullName) > 0 { + inParen := strings.HasSuffix(fullName, ")") + fullName = strings.TrimSuffix(fullName, ")") + + s := lastIdentRx.FindString(fullName) + if s == "" { + break + } + name = s + "." + name + fullName = strings.TrimSuffix(fullName, s) + + if i := strings.LastIndexByte(fullName, '('); inParen && i >= 0 { + fullName = fullName[:i] + } + fullName = strings.TrimSuffix(fullName, ".") + } + return strings.TrimSuffix(name, ".") +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/function/func_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/function/func_test.go new file mode 100644 index 0000000..f03ef45 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/function/func_test.go @@ -0,0 +1,51 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package function + +import ( + "bytes" + "reflect" + "testing" +) + +type myType struct{ bytes.Buffer } + +func (myType) valueMethod() {} +func (myType) ValueMethod() {} + +func (*myType) pointerMethod() {} +func (*myType) PointerMethod() {} + +func TestNameOf(t *testing.T) { + tests := []struct { + fnc interface{} + want string + }{ + {TestNameOf, "function.TestNameOf"}, + {func() {}, "function.TestNameOf.func1"}, + {(myType).valueMethod, "function.myType.valueMethod"}, + {(myType).ValueMethod, "function.myType.ValueMethod"}, + {(myType{}).valueMethod, "function.myType.valueMethod"}, + {(myType{}).ValueMethod, "function.myType.ValueMethod"}, + {(*myType).valueMethod, "function.myType.valueMethod"}, + {(*myType).ValueMethod, "function.myType.ValueMethod"}, + {(&myType{}).valueMethod, "function.myType.valueMethod"}, + {(&myType{}).ValueMethod, "function.myType.ValueMethod"}, + {(*myType).pointerMethod, "function.myType.pointerMethod"}, + {(*myType).PointerMethod, "function.myType.PointerMethod"}, + {(&myType{}).pointerMethod, "function.myType.pointerMethod"}, + {(&myType{}).PointerMethod, "function.myType.PointerMethod"}, + {(*myType).Write, "function.myType.Write"}, + {(&myType{}).Write, "bytes.Buffer.Write"}, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got := NameOf(reflect.ValueOf(tt.fnc)) + if got != tt.want { + t.Errorf("NameOf() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/testprotos/protos.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/testprotos/protos.go new file mode 100644 index 0000000..81622d3 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/testprotos/protos.go @@ -0,0 +1,116 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testprotos + +func Equal(x, y Message) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() +} + +type Message interface { + Proto() + String() string +} + +type proto interface { + Proto() +} + +type notComparable struct { + unexportedField func() +} + +type Stringer struct{ X string } + +func (s *Stringer) String() string { return s.X } + +// Project1 protocol buffers +type ( + Eagle_States int + Eagle_MissingCalls int + Dreamer_States int + Dreamer_MissingCalls int + Slap_States int + Goat_States int + Donkey_States int + SummerType int + + Eagle struct { + proto + notComparable + Stringer + } + Dreamer struct { + proto + notComparable + Stringer + } + Slap struct { + proto + notComparable + Stringer + } + Goat struct { + proto + notComparable + Stringer + } + Donkey struct { + proto + notComparable + Stringer + } +) + +// Project2 protocol buffers +type ( + Germ struct { + proto + notComparable + Stringer + } + Dish struct { + proto + notComparable + Stringer + } +) + +// Project3 protocol buffers +type ( + Dirt struct { + proto + notComparable + Stringer + } + Wizard struct { + proto + notComparable + Stringer + } + Sadistic struct { + proto + notComparable + Stringer + } +) + +// Project4 protocol buffers +type ( + HoneyStatus int + PoisonType int + MetaData struct { + proto + notComparable + Stringer + } + Restrictions struct { + proto + notComparable + Stringer + } +) diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/foo1/foo.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/foo1/foo.go new file mode 100644 index 0000000..c0882fb --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/foo1/foo.go @@ -0,0 +1,10 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package foo is deliberately named differently than the parent directory. +// It contain declarations that have ambiguity in their short names, +// relative to a different package also called foo. +package foo + +type Bar struct{ S string } diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/foo2/foo.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/foo2/foo.go new file mode 100644 index 0000000..c0882fb --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/foo2/foo.go @@ -0,0 +1,10 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package foo is deliberately named differently than the parent directory. +// It contain declarations that have ambiguity in their short names, +// relative to a different package also called foo. +package foo + +type Bar struct{ S string } diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project1.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project1.go new file mode 100644 index 0000000..223d6ab --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project1.go @@ -0,0 +1,267 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalEagle(x, y Eagle) bool { + if x.Name != y.Name && + !reflect.DeepEqual(x.Hounds, y.Hounds) && + x.Desc != y.Desc && + x.DescLong != y.DescLong && + x.Prong != y.Prong && + x.StateGoverner != y.StateGoverner && + x.PrankRating != y.PrankRating && + x.FunnyPrank != y.FunnyPrank && + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + return false + } + + if len(x.Dreamers) != len(y.Dreamers) { + return false + } + for i := range x.Dreamers { + if !equalDreamer(x.Dreamers[i], y.Dreamers[i]) { + return false + } + } + if len(x.Slaps) != len(y.Slaps) { + return false + } + for i := range x.Slaps { + if !equalSlap(x.Slaps[i], y.Slaps[i]) { + return false + } + } + return true +} +func equalDreamer(x, y Dreamer) bool { + if x.Name != y.Name || + x.Desc != y.Desc || + x.DescLong != y.DescLong || + x.ContSlapsInterval != y.ContSlapsInterval || + x.Ornamental != y.Ornamental || + x.Amoeba != y.Amoeba || + x.Heroes != y.Heroes || + x.FloppyDisk != y.FloppyDisk || + x.MightiestDuck != y.MightiestDuck || + x.FunnyPrank != y.FunnyPrank || + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + + return false + } + if len(x.Animal) != len(y.Animal) { + return false + } + for i := range x.Animal { + vx := x.Animal[i] + vy := y.Animal[i] + if reflect.TypeOf(x.Animal) != reflect.TypeOf(y.Animal) { + return false + } + switch vx.(type) { + case Goat: + if !equalGoat(vx.(Goat), vy.(Goat)) { + return false + } + case Donkey: + if !equalDonkey(vx.(Donkey), vy.(Donkey)) { + return false + } + default: + panic(fmt.Sprintf("unknown type: %T", vx)) + } + } + if len(x.PreSlaps) != len(y.PreSlaps) { + return false + } + for i := range x.PreSlaps { + if !equalSlap(x.PreSlaps[i], y.PreSlaps[i]) { + return false + } + } + if len(x.ContSlaps) != len(y.ContSlaps) { + return false + } + for i := range x.ContSlaps { + if !equalSlap(x.ContSlaps[i], y.ContSlaps[i]) { + return false + } + } + return true +} +func equalSlap(x, y Slap) bool { + return x.Name == y.Name && + x.Desc == y.Desc && + x.DescLong == y.DescLong && + pb.Equal(x.Args, y.Args) && + x.Tense == y.Tense && + x.Interval == y.Interval && + x.Homeland == y.Homeland && + x.FunnyPrank == y.FunnyPrank && + pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) +} +func equalGoat(x, y Goat) bool { + if x.Target != y.Target || + x.FunnyPrank != y.FunnyPrank || + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + return false + } + if len(x.Slaps) != len(y.Slaps) { + return false + } + for i := range x.Slaps { + if !equalSlap(x.Slaps[i], y.Slaps[i]) { + return false + } + } + return true +} +func equalDonkey(x, y Donkey) bool { + return x.Pause == y.Pause && + x.Sleep == y.Sleep && + x.FunnyPrank == y.FunnyPrank && + pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) +} +*/ + +type Eagle struct { + Name string + Hounds []string + Desc string + DescLong string + Dreamers []Dreamer + Prong int64 + Slaps []Slap + StateGoverner string + PrankRating string + FunnyPrank string + Immutable *EagleImmutable +} + +type EagleImmutable struct { + ID string + State *pb.Eagle_States + MissingCall *pb.Eagle_MissingCalls + Birthday time.Time + Death time.Time + Started time.Time + LastUpdate time.Time + Creator string + empty bool +} + +type Dreamer struct { + Name string + Desc string + DescLong string + PreSlaps []Slap + ContSlaps []Slap + ContSlapsInterval int32 + Animal []interface{} // Could be either Goat or Donkey + Ornamental bool + Amoeba int64 + Heroes int32 + FloppyDisk int32 + MightiestDuck bool + FunnyPrank string + Immutable *DreamerImmutable +} + +type DreamerImmutable struct { + ID string + State *pb.Dreamer_States + MissingCall *pb.Dreamer_MissingCalls + Calls int32 + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} + +type Slap struct { + Name string + Desc string + DescLong string + Args pb.Message + Tense int32 + Interval int32 + Homeland uint32 + FunnyPrank string + Immutable *SlapImmutable +} + +type SlapImmutable struct { + ID string + Out pb.Message + MildSlap bool + PrettyPrint string + State *pb.Slap_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + LoveRadius *LoveRadius + empty bool +} + +type Goat struct { + Target string + Slaps []Slap + FunnyPrank string + Immutable *GoatImmutable +} + +type GoatImmutable struct { + ID string + State *pb.Goat_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} +type Donkey struct { + Pause bool + Sleep int32 + FunnyPrank string + Immutable *DonkeyImmutable +} + +type DonkeyImmutable struct { + ID string + State *pb.Donkey_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} + +type LoveRadius struct { + Summer *SummerLove + empty bool +} + +type SummerLove struct { + Summary *SummerLoveSummary + empty bool +} + +type SummerLoveSummary struct { + Devices []string + ChangeType []pb.SummerType + empty bool +} + +func (EagleImmutable) Proto() *pb.Eagle { return nil } +func (DreamerImmutable) Proto() *pb.Dreamer { return nil } +func (SlapImmutable) Proto() *pb.Slap { return nil } +func (GoatImmutable) Proto() *pb.Goat { return nil } +func (DonkeyImmutable) Proto() *pb.Donkey { return nil } diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project2.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project2.go new file mode 100644 index 0000000..1616dd8 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project2.go @@ -0,0 +1,74 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalBatch(b1, b2 *GermBatch) bool { + for _, b := range []*GermBatch{b1, b2} { + for _, l := range b.DirtyGerms { + sort.Slice(l, func(i, j int) bool { return l[i].String() < l[j].String() }) + } + for _, l := range b.CleanGerms { + sort.Slice(l, func(i, j int) bool { return l[i].String() < l[j].String() }) + } + } + if !pb.DeepEqual(b1.DirtyGerms, b2.DirtyGerms) || + !pb.DeepEqual(b1.CleanGerms, b2.CleanGerms) || + !pb.DeepEqual(b1.GermMap, b2.GermMap) { + return false + } + if len(b1.DishMap) != len(b2.DishMap) { + return false + } + for id := range b1.DishMap { + kpb1, err1 := b1.DishMap[id].Proto() + kpb2, err2 := b2.DishMap[id].Proto() + if !pb.Equal(kpb1, kpb2) || !reflect.DeepEqual(err1, err2) { + return false + } + } + return b1.HasPreviousResult == b2.HasPreviousResult && + b1.DirtyID == b2.DirtyID && + b1.CleanID == b2.CleanID && + b1.GermStrain == b2.GermStrain && + b1.TotalDirtyGerms == b2.TotalDirtyGerms && + b1.InfectedAt.Equal(b2.InfectedAt) +} +*/ + +type GermBatch struct { + DirtyGerms, CleanGerms map[int32][]*pb.Germ + GermMap map[int32]*pb.Germ + DishMap map[int32]*Dish + HasPreviousResult bool + DirtyID, CleanID int32 + GermStrain int32 + TotalDirtyGerms int + InfectedAt time.Time +} + +type Dish struct { + pb *pb.Dish + err error +} + +func CreateDish(m *pb.Dish, err error) *Dish { + return &Dish{pb: m, err: err} +} + +func (d *Dish) Proto() (*pb.Dish, error) { + if d.err != nil { + return nil, d.err + } + return d.pb, nil +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project3.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project3.go new file mode 100644 index 0000000..9e56dfa --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project3.go @@ -0,0 +1,82 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package teststructs + +import ( + "sync" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalDirt(x, y *Dirt) bool { + if !reflect.DeepEqual(x.table, y.table) || + !reflect.DeepEqual(x.ts, y.ts) || + x.Discord != y.Discord || + !pb.Equal(&x.Proto, &y.Proto) || + len(x.wizard) != len(y.wizard) || + len(x.sadistic) != len(y.sadistic) || + x.lastTime != y.lastTime { + return false + } + for k, vx := range x.wizard { + vy, ok := y.wizard[k] + if !ok || !pb.Equal(vx, vy) { + return false + } + } + for k, vx := range x.sadistic { + vy, ok := y.sadistic[k] + if !ok || !pb.Equal(vx, vy) { + return false + } + } + return true +} +*/ + +type FakeMutex struct { + sync.Locker + x struct{} +} + +type Dirt struct { + table Table // Always concrete type of MockTable + ts Timestamp + Discord DiscordState + Proto pb.Dirt + wizard map[string]*pb.Wizard + sadistic map[string]*pb.Sadistic + lastTime int64 + mu FakeMutex +} + +type DiscordState int + +type Timestamp int64 + +func (d *Dirt) SetTable(t Table) { d.table = t } +func (d *Dirt) SetTimestamp(t Timestamp) { d.ts = t } +func (d *Dirt) SetWizard(m map[string]*pb.Wizard) { d.wizard = m } +func (d *Dirt) SetSadistic(m map[string]*pb.Sadistic) { d.sadistic = m } +func (d *Dirt) SetLastTime(t int64) { d.lastTime = t } + +type Table interface { + Operation1() error + Operation2() error + Operation3() error +} + +type MockTable struct { + state []string +} + +func CreateMockTable(s []string) *MockTable { return &MockTable{s} } +func (mt *MockTable) Operation1() error { return nil } +func (mt *MockTable) Operation2() error { return nil } +func (mt *MockTable) Operation3() error { return nil } +func (mt *MockTable) State() []string { return mt.state } diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project4.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project4.go new file mode 100644 index 0000000..a09aba2 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/project4.go @@ -0,0 +1,142 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalCartel(x, y Cartel) bool { + if !(equalHeadquarter(x.Headquarter, y.Headquarter) && + x.Source() == y.Source() && + x.CreationDate().Equal(y.CreationDate()) && + x.Boss() == y.Boss() && + x.LastCrimeDate().Equal(y.LastCrimeDate())) { + return false + } + if len(x.Poisons()) != len(y.Poisons()) { + return false + } + for i := range x.Poisons() { + if !equalPoison(*x.Poisons()[i], *y.Poisons()[i]) { + return false + } + } + return true +} +func equalHeadquarter(x, y Headquarter) bool { + xr, yr := x.Restrictions(), y.Restrictions() + return x.ID() == y.ID() && + x.Location() == y.Location() && + reflect.DeepEqual(x.SubDivisions(), y.SubDivisions()) && + x.IncorporatedDate().Equal(y.IncorporatedDate()) && + pb.Equal(x.MetaData(), y.MetaData()) && + bytes.Equal(x.PrivateMessage(), y.PrivateMessage()) && + bytes.Equal(x.PublicMessage(), y.PublicMessage()) && + x.HorseBack() == y.HorseBack() && + x.Rattle() == y.Rattle() && + x.Convulsion() == y.Convulsion() && + x.Expansion() == y.Expansion() && + x.Status() == y.Status() && + pb.Equal(&xr, &yr) && + x.CreationTime().Equal(y.CreationTime()) +} +func equalPoison(x, y Poison) bool { + return x.PoisonType() == y.PoisonType() && + x.Expiration().Equal(y.Expiration()) && + x.Manufacturer() == y.Manufacturer() && + x.Potency() == y.Potency() +} +*/ + +type Cartel struct { + Headquarter + source string + creationDate time.Time + boss string + lastCrimeDate time.Time + poisons []*Poison +} + +func (p Cartel) Source() string { return p.source } +func (p Cartel) CreationDate() time.Time { return p.creationDate } +func (p Cartel) Boss() string { return p.boss } +func (p Cartel) LastCrimeDate() time.Time { return p.lastCrimeDate } +func (p Cartel) Poisons() []*Poison { return p.poisons } + +func (p *Cartel) SetSource(x string) { p.source = x } +func (p *Cartel) SetCreationDate(x time.Time) { p.creationDate = x } +func (p *Cartel) SetBoss(x string) { p.boss = x } +func (p *Cartel) SetLastCrimeDate(x time.Time) { p.lastCrimeDate = x } +func (p *Cartel) SetPoisons(x []*Poison) { p.poisons = x } + +type Headquarter struct { + id uint64 + location string + subDivisions []string + incorporatedDate time.Time + metaData *pb.MetaData + privateMessage []byte + publicMessage []byte + horseBack string + rattle string + convulsion bool + expansion uint64 + status pb.HoneyStatus + restrictions pb.Restrictions + creationTime time.Time +} + +func (hq Headquarter) ID() uint64 { return hq.id } +func (hq Headquarter) Location() string { return hq.location } +func (hq Headquarter) SubDivisions() []string { return hq.subDivisions } +func (hq Headquarter) IncorporatedDate() time.Time { return hq.incorporatedDate } +func (hq Headquarter) MetaData() *pb.MetaData { return hq.metaData } +func (hq Headquarter) PrivateMessage() []byte { return hq.privateMessage } +func (hq Headquarter) PublicMessage() []byte { return hq.publicMessage } +func (hq Headquarter) HorseBack() string { return hq.horseBack } +func (hq Headquarter) Rattle() string { return hq.rattle } +func (hq Headquarter) Convulsion() bool { return hq.convulsion } +func (hq Headquarter) Expansion() uint64 { return hq.expansion } +func (hq Headquarter) Status() pb.HoneyStatus { return hq.status } +func (hq Headquarter) Restrictions() pb.Restrictions { return hq.restrictions } +func (hq Headquarter) CreationTime() time.Time { return hq.creationTime } + +func (hq *Headquarter) SetID(x uint64) { hq.id = x } +func (hq *Headquarter) SetLocation(x string) { hq.location = x } +func (hq *Headquarter) SetSubDivisions(x []string) { hq.subDivisions = x } +func (hq *Headquarter) SetIncorporatedDate(x time.Time) { hq.incorporatedDate = x } +func (hq *Headquarter) SetMetaData(x *pb.MetaData) { hq.metaData = x } +func (hq *Headquarter) SetPrivateMessage(x []byte) { hq.privateMessage = x } +func (hq *Headquarter) SetPublicMessage(x []byte) { hq.publicMessage = x } +func (hq *Headquarter) SetHorseBack(x string) { hq.horseBack = x } +func (hq *Headquarter) SetRattle(x string) { hq.rattle = x } +func (hq *Headquarter) SetConvulsion(x bool) { hq.convulsion = x } +func (hq *Headquarter) SetExpansion(x uint64) { hq.expansion = x } +func (hq *Headquarter) SetStatus(x pb.HoneyStatus) { hq.status = x } +func (hq *Headquarter) SetRestrictions(x pb.Restrictions) { hq.restrictions = x } +func (hq *Headquarter) SetCreationTime(x time.Time) { hq.creationTime = x } + +type Poison struct { + poisonType pb.PoisonType + expiration time.Time + manufacturer string + potency int +} + +func (p Poison) PoisonType() pb.PoisonType { return p.poisonType } +func (p Poison) Expiration() time.Time { return p.expiration } +func (p Poison) Manufacturer() string { return p.manufacturer } +func (p Poison) Potency() int { return p.potency } + +func (p *Poison) SetPoisonType(x pb.PoisonType) { p.poisonType = x } +func (p *Poison) SetExpiration(x time.Time) { p.expiration = x } +func (p *Poison) SetManufacturer(x string) { p.manufacturer = x } +func (p *Poison) SetPotency(x int) { p.potency = x } diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/structs.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/structs.go new file mode 100644 index 0000000..bfd2de8 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/teststructs/structs.go @@ -0,0 +1,197 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package teststructs + +type InterfaceA interface { + InterfaceA() +} + +type ( + StructA struct{ X string } // Equal method on value receiver + StructB struct{ X string } // Equal method on pointer receiver + StructC struct{ X string } // Equal method (with interface argument) on value receiver + StructD struct{ X string } // Equal method (with interface argument) on pointer receiver + StructE struct{ X string } // Equal method (with interface argument on value receiver) on pointer receiver + StructF struct{ X string } // Equal method (with interface argument on pointer receiver) on value receiver + + // These embed the above types as a value. + StructA1 struct { + StructA + X string + } + StructB1 struct { + StructB + X string + } + StructC1 struct { + StructC + X string + } + StructD1 struct { + StructD + X string + } + StructE1 struct { + StructE + X string + } + StructF1 struct { + StructF + X string + } + + // These embed the above types as a pointer. + StructA2 struct { + *StructA + X string + } + StructB2 struct { + *StructB + X string + } + StructC2 struct { + *StructC + X string + } + StructD2 struct { + *StructD + X string + } + StructE2 struct { + *StructE + X string + } + StructF2 struct { + *StructF + X string + } + + StructNo struct{ X string } // Equal method (with interface argument) on non-satisfying receiver + + AssignA func() int + AssignB struct{ A int } + AssignC chan bool + AssignD <-chan bool +) + +func (x StructA) Equal(y StructA) bool { return true } +func (x *StructB) Equal(y *StructB) bool { return true } +func (x StructC) Equal(y InterfaceA) bool { return true } +func (x StructC) InterfaceA() {} +func (x *StructD) Equal(y InterfaceA) bool { return true } +func (x *StructD) InterfaceA() {} +func (x *StructE) Equal(y InterfaceA) bool { return true } +func (x StructE) InterfaceA() {} +func (x StructF) Equal(y InterfaceA) bool { return true } +func (x *StructF) InterfaceA() {} +func (x StructNo) Equal(y InterfaceA) bool { return true } + +func (x AssignA) Equal(y func() int) bool { return true } +func (x AssignB) Equal(y struct{ A int }) bool { return true } +func (x AssignC) Equal(y chan bool) bool { return true } +func (x AssignD) Equal(y <-chan bool) bool { return true } + +var _ = func( + a StructA, b StructB, c StructC, d StructD, e StructE, f StructF, + ap *StructA, bp *StructB, cp *StructC, dp *StructD, ep *StructE, fp *StructF, + a1 StructA1, b1 StructB1, c1 StructC1, d1 StructD1, e1 StructE1, f1 StructF1, + a2 StructA2, b2 StructB2, c2 StructC2, d2 StructD2, e2 StructE2, f2 StructF1, +) { + a.Equal(a) + b.Equal(&b) + c.Equal(c) + d.Equal(&d) + e.Equal(e) + f.Equal(&f) + + ap.Equal(*ap) + bp.Equal(bp) + cp.Equal(*cp) + dp.Equal(dp) + ep.Equal(*ep) + fp.Equal(fp) + + a1.Equal(a1.StructA) + b1.Equal(&b1.StructB) + c1.Equal(c1) + d1.Equal(&d1) + e1.Equal(e1) + f1.Equal(&f1) + + a2.Equal(*a2.StructA) + b2.Equal(b2.StructB) + c2.Equal(c2) + d2.Equal(&d2) + e2.Equal(e2) + f2.Equal(&f2) +} + +type ( + privateStruct struct{ Public, private int } + PublicStruct struct{ Public, private int } + ParentStructA struct{ privateStruct } + ParentStructB struct{ PublicStruct } + ParentStructC struct { + privateStruct + Public, private int + } + ParentStructD struct { + PublicStruct + Public, private int + } + ParentStructE struct { + privateStruct + PublicStruct + } + ParentStructF struct { + privateStruct + PublicStruct + Public, private int + } + ParentStructG struct { + *privateStruct + } + ParentStructH struct { + *PublicStruct + } + ParentStructI struct { + *privateStruct + *PublicStruct + } + ParentStructJ struct { + *privateStruct + *PublicStruct + Public PublicStruct + private privateStruct + } +) + +func NewParentStructG() *ParentStructG { + return &ParentStructG{new(privateStruct)} +} +func NewParentStructH() *ParentStructH { + return &ParentStructH{new(PublicStruct)} +} +func NewParentStructI() *ParentStructI { + return &ParentStructI{new(privateStruct), new(PublicStruct)} +} +func NewParentStructJ() *ParentStructJ { + return &ParentStructJ{ + privateStruct: new(privateStruct), PublicStruct: new(PublicStruct), + } +} +func (s *privateStruct) SetPrivate(i int) { s.private = i } +func (s *PublicStruct) SetPrivate(i int) { s.private = i } +func (s *ParentStructC) SetPrivate(i int) { s.private = i } +func (s *ParentStructD) SetPrivate(i int) { s.private = i } +func (s *ParentStructF) SetPrivate(i int) { s.private = i } +func (s *ParentStructA) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructC) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructE) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructF) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructG) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructI) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructJ) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructJ) Private() *privateStruct { return &s.private } diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/name.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/name.go new file mode 100644 index 0000000..b6c12ce --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/name.go @@ -0,0 +1,157 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value + +import ( + "reflect" + "strconv" +) + +// TypeString is nearly identical to reflect.Type.String, +// but has an additional option to specify that full type names be used. +func TypeString(t reflect.Type, qualified bool) string { + return string(appendTypeName(nil, t, qualified, false)) +} + +func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte { + // BUG: Go reflection provides no way to disambiguate two named types + // of the same name and within the same package, + // but declared within the namespace of different functions. + + // Named type. + if t.Name() != "" { + if qualified && t.PkgPath() != "" { + b = append(b, '"') + b = append(b, t.PkgPath()...) + b = append(b, '"') + b = append(b, '.') + b = append(b, t.Name()...) + } else { + b = append(b, t.String()...) + } + return b + } + + // Unnamed type. + switch k := t.Kind(); k { + case reflect.Bool, reflect.String, reflect.UnsafePointer, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + b = append(b, k.String()...) + case reflect.Chan: + if t.ChanDir() == reflect.RecvDir { + b = append(b, "<-"...) + } + b = append(b, "chan"...) + if t.ChanDir() == reflect.SendDir { + b = append(b, "<-"...) + } + b = append(b, ' ') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Func: + if !elideFunc { + b = append(b, "func"...) + } + b = append(b, '(') + for i := 0; i < t.NumIn(); i++ { + if i > 0 { + b = append(b, ", "...) + } + if i == t.NumIn()-1 && t.IsVariadic() { + b = append(b, "..."...) + b = appendTypeName(b, t.In(i).Elem(), qualified, false) + } else { + b = appendTypeName(b, t.In(i), qualified, false) + } + } + b = append(b, ')') + switch t.NumOut() { + case 0: + // Do nothing + case 1: + b = append(b, ' ') + b = appendTypeName(b, t.Out(0), qualified, false) + default: + b = append(b, " ("...) + for i := 0; i < t.NumOut(); i++ { + if i > 0 { + b = append(b, ", "...) + } + b = appendTypeName(b, t.Out(i), qualified, false) + } + b = append(b, ')') + } + case reflect.Struct: + b = append(b, "struct{ "...) + for i := 0; i < t.NumField(); i++ { + if i > 0 { + b = append(b, "; "...) + } + sf := t.Field(i) + if !sf.Anonymous { + if qualified && sf.PkgPath != "" { + b = append(b, '"') + b = append(b, sf.PkgPath...) + b = append(b, '"') + b = append(b, '.') + } + b = append(b, sf.Name...) + b = append(b, ' ') + } + b = appendTypeName(b, sf.Type, qualified, false) + if sf.Tag != "" { + b = append(b, ' ') + b = strconv.AppendQuote(b, string(sf.Tag)) + } + } + if b[len(b)-1] == ' ' { + b = b[:len(b)-1] + } else { + b = append(b, ' ') + } + b = append(b, '}') + case reflect.Slice, reflect.Array: + b = append(b, '[') + if k == reflect.Array { + b = strconv.AppendUint(b, uint64(t.Len()), 10) + } + b = append(b, ']') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Map: + b = append(b, "map["...) + b = appendTypeName(b, t.Key(), qualified, false) + b = append(b, ']') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Ptr: + b = append(b, '*') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Interface: + b = append(b, "interface{ "...) + for i := 0; i < t.NumMethod(); i++ { + if i > 0 { + b = append(b, "; "...) + } + m := t.Method(i) + if qualified && m.PkgPath != "" { + b = append(b, '"') + b = append(b, m.PkgPath...) + b = append(b, '"') + b = append(b, '.') + } + b = append(b, m.Name...) + b = appendTypeName(b, m.Type, qualified, true) + } + if b[len(b)-1] == ' ' { + b = b[:len(b)-1] + } else { + b = append(b, ' ') + } + b = append(b, '}') + default: + panic("invalid kind: " + k.String()) + } + return b +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/name_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/name_test.go new file mode 100644 index 0000000..3eec91c --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/name_test.go @@ -0,0 +1,144 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value + +import ( + "reflect" + "strings" + "testing" +) + +type Named struct{} + +var pkgPath = reflect.TypeOf(Named{}).PkgPath() + +func TestTypeString(t *testing.T) { + tests := []struct { + in interface{} + want string + }{{ + in: bool(false), + want: "bool", + }, { + in: int(0), + want: "int", + }, { + in: float64(0), + want: "float64", + }, { + in: string(""), + want: "string", + }, { + in: Named{}, + want: "$PackagePath.Named", + }, { + in: (chan Named)(nil), + want: "chan $PackagePath.Named", + }, { + in: (<-chan Named)(nil), + want: "<-chan $PackagePath.Named", + }, { + in: (chan<- Named)(nil), + want: "chan<- $PackagePath.Named", + }, { + in: (func())(nil), + want: "func()", + }, { + in: (func(Named))(nil), + want: "func($PackagePath.Named)", + }, { + in: (func() Named)(nil), + want: "func() $PackagePath.Named", + }, { + in: (func(int, Named) (int, error))(nil), + want: "func(int, $PackagePath.Named) (int, error)", + }, { + in: (func(...Named))(nil), + want: "func(...$PackagePath.Named)", + }, { + in: struct{}{}, + want: "struct{}", + }, { + in: struct{ Named }{}, + want: "struct{ $PackagePath.Named }", + }, { + in: struct { + Named `tag` + }{}, + want: "struct{ $PackagePath.Named \"tag\" }", + }, { + in: struct{ Named Named }{}, + want: "struct{ Named $PackagePath.Named }", + }, { + in: struct { + Named Named `tag` + }{}, + want: "struct{ Named $PackagePath.Named \"tag\" }", + }, { + in: struct { + Int int + Named Named + }{}, + want: "struct{ Int int; Named $PackagePath.Named }", + }, { + in: struct { + _ int + x Named + }{}, + want: "struct{ $FieldPrefix._ int; $FieldPrefix.x $PackagePath.Named }", + }, { + in: []Named(nil), + want: "[]$PackagePath.Named", + }, { + in: []*Named(nil), + want: "[]*$PackagePath.Named", + }, { + in: [10]Named{}, + want: "[10]$PackagePath.Named", + }, { + in: [10]*Named{}, + want: "[10]*$PackagePath.Named", + }, { + in: map[string]string(nil), + want: "map[string]string", + }, { + in: map[Named]Named(nil), + want: "map[$PackagePath.Named]$PackagePath.Named", + }, { + in: (*Named)(nil), + want: "*$PackagePath.Named", + }, { + in: (*interface{})(nil), + want: "*interface{}", + }, { + in: (*interface{ Read([]byte) (int, error) })(nil), + want: "*interface{ Read([]uint8) (int, error) }", + }, { + in: (*interface { + F1() + F2(Named) + F3() Named + F4(int, Named) (int, error) + F5(...Named) + })(nil), + want: "*interface{ F1(); F2($PackagePath.Named); F3() $PackagePath.Named; F4(int, $PackagePath.Named) (int, error); F5(...$PackagePath.Named) }", + }} + + for _, tt := range tests { + typ := reflect.TypeOf(tt.in) + wantShort := tt.want + wantShort = strings.Replace(wantShort, "$PackagePath", "value", -1) + wantShort = strings.Replace(wantShort, "$FieldPrefix.", "", -1) + if gotShort := TypeString(typ, false); gotShort != wantShort { + t.Errorf("TypeString(%v, false) mismatch:\ngot: %v\nwant: %v", typ, gotShort, wantShort) + } + wantQualified := tt.want + wantQualified = strings.Replace(wantQualified, "$PackagePath", `"`+pkgPath+`"`, -1) + wantQualified = strings.Replace(wantQualified, "$FieldPrefix", `"`+pkgPath+`"`, -1) + if gotQualified := TypeString(typ, true); gotQualified != wantQualified { + t.Errorf("TypeString(%v, true) mismatch:\ngot: %v\nwant: %v", typ, gotQualified, wantQualified) + } + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/pointer_purego.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/pointer_purego.go new file mode 100644 index 0000000..44f4a5a --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/pointer_purego.go @@ -0,0 +1,33 @@ +// Copyright 2018, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build purego + +package value + +import "reflect" + +// Pointer is an opaque typed pointer and is guaranteed to be comparable. +type Pointer struct { + p uintptr + t reflect.Type +} + +// PointerOf returns a Pointer from v, which must be a +// reflect.Ptr, reflect.Slice, or reflect.Map. +func PointerOf(v reflect.Value) Pointer { + // NOTE: Storing a pointer as an uintptr is technically incorrect as it + // assumes that the GC implementation does not use a moving collector. + return Pointer{v.Pointer(), v.Type()} +} + +// IsNil reports whether the pointer is nil. +func (p Pointer) IsNil() bool { + return p.p == 0 +} + +// Uintptr returns the pointer as a uintptr. +func (p Pointer) Uintptr() uintptr { + return p.p +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/pointer_unsafe.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/pointer_unsafe.go new file mode 100644 index 0000000..a605953 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/pointer_unsafe.go @@ -0,0 +1,36 @@ +// Copyright 2018, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !purego + +package value + +import ( + "reflect" + "unsafe" +) + +// Pointer is an opaque typed pointer and is guaranteed to be comparable. +type Pointer struct { + p unsafe.Pointer + t reflect.Type +} + +// PointerOf returns a Pointer from v, which must be a +// reflect.Ptr, reflect.Slice, or reflect.Map. +func PointerOf(v reflect.Value) Pointer { + // The proper representation of a pointer is unsafe.Pointer, + // which is necessary if the GC ever uses a moving collector. + return Pointer{unsafe.Pointer(v.Pointer()), v.Type()} +} + +// IsNil reports whether the pointer is nil. +func (p Pointer) IsNil() bool { + return p.p == nil +} + +// Uintptr returns the pointer as a uintptr. +func (p Pointer) Uintptr() uintptr { + return uintptr(p.p) +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/sort.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/sort.go new file mode 100644 index 0000000..98533b0 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/sort.go @@ -0,0 +1,106 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value + +import ( + "fmt" + "math" + "reflect" + "sort" +) + +// SortKeys sorts a list of map keys, deduplicating keys if necessary. +// The type of each value must be comparable. +func SortKeys(vs []reflect.Value) []reflect.Value { + if len(vs) == 0 { + return vs + } + + // Sort the map keys. + sort.SliceStable(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) + + // Deduplicate keys (fails for NaNs). + vs2 := vs[:1] + for _, v := range vs[1:] { + if isLess(vs2[len(vs2)-1], v) { + vs2 = append(vs2, v) + } + } + return vs2 +} + +// isLess is a generic function for sorting arbitrary map keys. +// The inputs must be of the same type and must be comparable. +func isLess(x, y reflect.Value) bool { + switch x.Type().Kind() { + case reflect.Bool: + return !x.Bool() && y.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return x.Int() < y.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return x.Uint() < y.Uint() + case reflect.Float32, reflect.Float64: + // NOTE: This does not sort -0 as less than +0 + // since Go maps treat -0 and +0 as equal keys. + fx, fy := x.Float(), y.Float() + return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) + case reflect.Complex64, reflect.Complex128: + cx, cy := x.Complex(), y.Complex() + rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) + if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { + return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) + } + return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) + case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: + return x.Pointer() < y.Pointer() + case reflect.String: + return x.String() < y.String() + case reflect.Array: + for i := 0; i < x.Len(); i++ { + if isLess(x.Index(i), y.Index(i)) { + return true + } + if isLess(y.Index(i), x.Index(i)) { + return false + } + } + return false + case reflect.Struct: + for i := 0; i < x.NumField(); i++ { + if isLess(x.Field(i), y.Field(i)) { + return true + } + if isLess(y.Field(i), x.Field(i)) { + return false + } + } + return false + case reflect.Interface: + vx, vy := x.Elem(), y.Elem() + if !vx.IsValid() || !vy.IsValid() { + return !vx.IsValid() && vy.IsValid() + } + tx, ty := vx.Type(), vy.Type() + if tx == ty { + return isLess(x.Elem(), y.Elem()) + } + if tx.Kind() != ty.Kind() { + return vx.Kind() < vy.Kind() + } + if tx.String() != ty.String() { + return tx.String() < ty.String() + } + if tx.PkgPath() != ty.PkgPath() { + return tx.PkgPath() < ty.PkgPath() + } + // This can happen in rare situations, so we fallback to just comparing + // the unique pointer for a reflect.Type. This guarantees deterministic + // ordering within a program, but it is obviously not stable. + return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() + default: + // Must be Func, Map, or Slice; which are not comparable. + panic(fmt.Sprintf("%T is not comparable", x.Type())) + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/sort_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/sort_test.go new file mode 100644 index 0000000..26222d6 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/sort_test.go @@ -0,0 +1,159 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value_test + +import ( + "math" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/value" +) + +func TestSortKeys(t *testing.T) { + type ( + MyString string + MyArray [2]int + MyStruct struct { + A MyString + B MyArray + C chan float64 + } + EmptyStruct struct{} + ) + + opts := []cmp.Option{ + cmp.Comparer(func(x, y float64) bool { + if math.IsNaN(x) && math.IsNaN(y) { + return true + } + return x == y + }), + cmp.Comparer(func(x, y complex128) bool { + rx, ix, ry, iy := real(x), imag(x), real(y), imag(y) + if math.IsNaN(rx) && math.IsNaN(ry) { + rx, ry = 0, 0 + } + if math.IsNaN(ix) && math.IsNaN(iy) { + ix, iy = 0, 0 + } + return rx == ry && ix == iy + }), + cmp.Comparer(func(x, y chan bool) bool { return true }), + cmp.Comparer(func(x, y chan int) bool { return true }), + cmp.Comparer(func(x, y chan float64) bool { return true }), + cmp.Comparer(func(x, y chan interface{}) bool { return true }), + cmp.Comparer(func(x, y *int) bool { return true }), + } + + tests := []struct { + in map[interface{}]bool // Set of keys to sort + want []interface{} + }{{ + in: map[interface{}]bool{1: true, 2: true, 3: true}, + want: []interface{}{1, 2, 3}, + }, { + in: map[interface{}]bool{ + nil: true, + true: true, + false: true, + -5: true, + -55: true, + -555: true, + uint(1): true, + uint(11): true, + uint(111): true, + "abc": true, + "abcd": true, + "abcde": true, + "foo": true, + "bar": true, + MyString("abc"): true, + MyString("abcd"): true, + MyString("abcde"): true, + new(int): true, + new(int): true, + make(chan bool): true, + make(chan bool): true, + make(chan int): true, + make(chan interface{}): true, + math.Inf(+1): true, + math.Inf(-1): true, + 1.2345: true, + 12.345: true, + 123.45: true, + 1234.5: true, + 0 + 0i: true, + 1 + 0i: true, + 2 + 0i: true, + 0 + 1i: true, + 0 + 2i: true, + 0 + 3i: true, + [2]int{2, 3}: true, + [2]int{4, 0}: true, + [2]int{2, 4}: true, + MyArray([2]int{2, 4}): true, + EmptyStruct{}: true, + MyStruct{ + "bravo", [2]int{2, 3}, make(chan float64), + }: true, + MyStruct{ + "alpha", [2]int{3, 3}, make(chan float64), + }: true, + }, + want: []interface{}{ + nil, false, true, + -555, -55, -5, uint(1), uint(11), uint(111), + math.Inf(-1), 1.2345, 12.345, 123.45, 1234.5, math.Inf(+1), + (0 + 0i), (0 + 1i), (0 + 2i), (0 + 3i), (1 + 0i), (2 + 0i), + [2]int{2, 3}, [2]int{2, 4}, [2]int{4, 0}, MyArray([2]int{2, 4}), + make(chan bool), make(chan bool), make(chan int), make(chan interface{}), + new(int), new(int), + "abc", "abcd", "abcde", "bar", "foo", + MyString("abc"), MyString("abcd"), MyString("abcde"), + EmptyStruct{}, + MyStruct{"alpha", [2]int{3, 3}, make(chan float64)}, + MyStruct{"bravo", [2]int{2, 3}, make(chan float64)}, + }, + }, { + // NaN values cannot be properly deduplicated. + // This is okay since map entries with NaN in the keys cannot be + // retrieved anyways. + in: map[interface{}]bool{ + math.NaN(): true, + math.NaN(): true, + complex(0, math.NaN()): true, + complex(0, math.NaN()): true, + complex(math.NaN(), 0): true, + complex(math.NaN(), 0): true, + complex(math.NaN(), math.NaN()): true, + }, + want: []interface{}{ + math.NaN(), + complex(math.NaN(), math.NaN()), + complex(math.NaN(), 0), + complex(0, math.NaN()), + }, + }} + + for i, tt := range tests { + // Intentionally pass the map via an unexported field to detect panics. + // Unfortunately, we cannot actually test the keys without using unsafe. + v := reflect.ValueOf(struct{ x map[interface{}]bool }{tt.in}).Field(0) + value.SortKeys(append(v.MapKeys(), v.MapKeys()...)) + + // Try again, with keys that have read-write access in reflect. + v = reflect.ValueOf(tt.in) + keys := append(v.MapKeys(), v.MapKeys()...) + var got []interface{} + for _, k := range value.SortKeys(keys) { + got = append(got, k.Interface()) + } + if d := cmp.Diff(got, tt.want, opts...); d != "" { + t.Errorf("test %d, Sort() mismatch (-got +want):\n%s", i, d) + } + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/zero.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/zero.go new file mode 100644 index 0000000..9147a29 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/zero.go @@ -0,0 +1,48 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value + +import ( + "math" + "reflect" +) + +// IsZero reports whether v is the zero value. +// This does not rely on Interface and so can be used on unexported fields. +func IsZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() == false + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return math.Float64bits(v.Float()) == 0 + case reflect.Complex64, reflect.Complex128: + return math.Float64bits(real(v.Complex())) == 0 && math.Float64bits(imag(v.Complex())) == 0 + case reflect.String: + return v.String() == "" + case reflect.UnsafePointer: + return v.Pointer() == 0 + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if !IsZero(v.Index(i)) { + return false + } + } + return true + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if !IsZero(v.Field(i)) { + return false + } + } + return true + } + return false +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/zero_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/zero_test.go new file mode 100644 index 0000000..ddaa337 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/internal/value/zero_test.go @@ -0,0 +1,52 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value + +import ( + "archive/tar" + "math" + "reflect" + "testing" +) + +func TestIsZero(t *testing.T) { + tests := []struct { + in interface{} + want bool + }{ + {0, true}, + {1, false}, + {"", true}, + {"foo", false}, + {[]byte(nil), true}, + {[]byte{}, false}, + {map[string]bool(nil), true}, + {map[string]bool{}, false}, + {tar.Header{}, true}, + {&tar.Header{}, false}, + {tar.Header{Name: "foo"}, false}, + {(chan bool)(nil), true}, + {make(chan bool), false}, + {(func(*testing.T))(nil), true}, + {TestIsZero, false}, + {[...]int{0, 0, 0}, true}, + {[...]int{0, 1, 0}, false}, + {math.Copysign(0, +1), true}, + {math.Copysign(0, -1), false}, + {complex(math.Copysign(0, +1), math.Copysign(0, +1)), true}, + {complex(math.Copysign(0, -1), math.Copysign(0, +1)), false}, + {complex(math.Copysign(0, +1), math.Copysign(0, -1)), false}, + {complex(math.Copysign(0, -1), math.Copysign(0, -1)), false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got := IsZero(reflect.ValueOf(tt.in)) + if got != tt.want { + t.Errorf("IsZero(%v) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/options.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/options.go new file mode 100644 index 0000000..e57b9eb --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/options.go @@ -0,0 +1,552 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/google/go-cmp/cmp/internal/function" +) + +// Option configures for specific behavior of Equal and Diff. In particular, +// the fundamental Option functions (Ignore, Transformer, and Comparer), +// configure how equality is determined. +// +// The fundamental options may be composed with filters (FilterPath and +// FilterValues) to control the scope over which they are applied. +// +// The cmp/cmpopts package provides helper functions for creating options that +// may be used with Equal and Diff. +type Option interface { + // filter applies all filters and returns the option that remains. + // Each option may only read s.curPath and call s.callTTBFunc. + // + // An Options is returned only if multiple comparers or transformers + // can apply simultaneously and will only contain values of those types + // or sub-Options containing values of those types. + filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption +} + +// applicableOption represents the following types: +// Fundamental: ignore | validator | *comparer | *transformer +// Grouping: Options +type applicableOption interface { + Option + + // apply executes the option, which may mutate s or panic. + apply(s *state, vx, vy reflect.Value) +} + +// coreOption represents the following types: +// Fundamental: ignore | validator | *comparer | *transformer +// Filters: *pathFilter | *valuesFilter +type coreOption interface { + Option + isCore() +} + +type core struct{} + +func (core) isCore() {} + +// Options is a list of Option values that also satisfies the Option interface. +// Helper comparison packages may return an Options value when packing multiple +// Option values into a single Option. When this package processes an Options, +// it will be implicitly expanded into a flat list. +// +// Applying a filter on an Options is equivalent to applying that same filter +// on all individual options held within. +type Options []Option + +func (opts Options) filter(s *state, t reflect.Type, vx, vy reflect.Value) (out applicableOption) { + for _, opt := range opts { + switch opt := opt.filter(s, t, vx, vy); opt.(type) { + case ignore: + return ignore{} // Only ignore can short-circuit evaluation + case validator: + out = validator{} // Takes precedence over comparer or transformer + case *comparer, *transformer, Options: + switch out.(type) { + case nil: + out = opt + case validator: + // Keep validator + case *comparer, *transformer, Options: + out = Options{out, opt} // Conflicting comparers or transformers + } + } + } + return out +} + +func (opts Options) apply(s *state, _, _ reflect.Value) { + const warning = "ambiguous set of applicable options" + const help = "consider using filters to ensure at most one Comparer or Transformer may apply" + var ss []string + for _, opt := range flattenOptions(nil, opts) { + ss = append(ss, fmt.Sprint(opt)) + } + set := strings.Join(ss, "\n\t") + panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help)) +} + +func (opts Options) String() string { + var ss []string + for _, opt := range opts { + ss = append(ss, fmt.Sprint(opt)) + } + return fmt.Sprintf("Options{%s}", strings.Join(ss, ", ")) +} + +// FilterPath returns a new Option where opt is only evaluated if filter f +// returns true for the current Path in the value tree. +// +// This filter is called even if a slice element or map entry is missing and +// provides an opportunity to ignore such cases. The filter function must be +// symmetric such that the filter result is identical regardless of whether the +// missing value is from x or y. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterPath(f func(Path) bool, opt Option) Option { + if f == nil { + panic("invalid path filter function") + } + if opt := normalizeOption(opt); opt != nil { + return &pathFilter{fnc: f, opt: opt} + } + return nil +} + +type pathFilter struct { + core + fnc func(Path) bool + opt Option +} + +func (f pathFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { + if f.fnc(s.curPath) { + return f.opt.filter(s, t, vx, vy) + } + return nil +} + +func (f pathFilter) String() string { + return fmt.Sprintf("FilterPath(%s, %v)", function.NameOf(reflect.ValueOf(f.fnc)), f.opt) +} + +// FilterValues returns a new Option where opt is only evaluated if filter f, +// which is a function of the form "func(T, T) bool", returns true for the +// current pair of values being compared. If either value is invalid or +// the type of the values is not assignable to T, then this filter implicitly +// returns false. +// +// The filter function must be +// symmetric (i.e., agnostic to the order of the inputs) and +// deterministic (i.e., produces the same result when given the same inputs). +// If T is an interface, it is possible that f is called with two values with +// different concrete types that both implement T. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterValues(f interface{}, opt Option) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() { + panic(fmt.Sprintf("invalid values filter function: %T", f)) + } + if opt := normalizeOption(opt); opt != nil { + vf := &valuesFilter{fnc: v, opt: opt} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + vf.typ = ti + } + return vf + } + return nil +} + +type valuesFilter struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool + opt Option +} + +func (f valuesFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { + if !vx.IsValid() || !vx.CanInterface() || !vy.IsValid() || !vy.CanInterface() { + return nil + } + if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { + return f.opt.filter(s, t, vx, vy) + } + return nil +} + +func (f valuesFilter) String() string { + return fmt.Sprintf("FilterValues(%s, %v)", function.NameOf(f.fnc), f.opt) +} + +// Ignore is an Option that causes all comparisons to be ignored. +// This value is intended to be combined with FilterPath or FilterValues. +// It is an error to pass an unfiltered Ignore option to Equal. +func Ignore() Option { return ignore{} } + +type ignore struct{ core } + +func (ignore) isFiltered() bool { return false } +func (ignore) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { return ignore{} } +func (ignore) apply(s *state, _, _ reflect.Value) { s.report(true, reportByIgnore) } +func (ignore) String() string { return "Ignore()" } + +// validator is a sentinel Option type to indicate that some options could not +// be evaluated due to unexported fields, missing slice elements, or +// missing map entries. Both values are validator only for unexported fields. +type validator struct{ core } + +func (validator) filter(_ *state, _ reflect.Type, vx, vy reflect.Value) applicableOption { + if !vx.IsValid() || !vy.IsValid() { + return validator{} + } + if !vx.CanInterface() || !vy.CanInterface() { + return validator{} + } + return nil +} +func (validator) apply(s *state, vx, vy reflect.Value) { + // Implies missing slice element or map entry. + if !vx.IsValid() || !vy.IsValid() { + s.report(vx.IsValid() == vy.IsValid(), 0) + return + } + + // Unable to Interface implies unexported field without visibility access. + if !vx.CanInterface() || !vy.CanInterface() { + help := "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported" + var name string + if t := s.curPath.Index(-2).Type(); t.Name() != "" { + // Named type with unexported fields. + name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType + if _, ok := reflect.New(t).Interface().(error); ok { + help = "consider using cmpopts.EquateErrors to compare error values" + } + } else { + // Unnamed type with unexported fields. Derive PkgPath from field. + var pkgPath string + for i := 0; i < t.NumField() && pkgPath == ""; i++ { + pkgPath = t.Field(i).PkgPath + } + name = fmt.Sprintf("%q.(%v)", pkgPath, t.String()) // e.g., "path/to/package".(struct { a int }) + } + panic(fmt.Sprintf("cannot handle unexported field at %#v:\n\t%v\n%s", s.curPath, name, help)) + } + + panic("not reachable") +} + +// identRx represents a valid identifier according to the Go specification. +const identRx = `[_\p{L}][_\p{L}\p{N}]*` + +var identsRx = regexp.MustCompile(`^` + identRx + `(\.` + identRx + `)*$`) + +// Transformer returns an Option that applies a transformation function that +// converts values of a certain type into that of another. +// +// The transformer f must be a function "func(T) R" that converts values of +// type T to those of type R and is implicitly filtered to input values +// assignable to T. The transformer must not mutate T in any way. +// +// To help prevent some cases of infinite recursive cycles applying the +// same transform to the output of itself (e.g., in the case where the +// input and output types are the same), an implicit filter is added such that +// a transformer is applicable only if that exact transformer is not already +// in the tail of the Path since the last non-Transform step. +// For situations where the implicit filter is still insufficient, +// consider using cmpopts.AcyclicTransformer, which adds a filter +// to prevent the transformer from being recursively applied upon itself. +// +// The name is a user provided label that is used as the Transform.Name in the +// transformation PathStep (and eventually shown in the Diff output). +// The name must be a valid identifier or qualified identifier in Go syntax. +// If empty, an arbitrary name is used. +func Transformer(name string, f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Transformer) || v.IsNil() { + panic(fmt.Sprintf("invalid transformer function: %T", f)) + } + if name == "" { + name = function.NameOf(v) + if !identsRx.MatchString(name) { + name = "λ" // Lambda-symbol as placeholder name + } + } else if !identsRx.MatchString(name) { + panic(fmt.Sprintf("invalid name: %q", name)) + } + tr := &transformer{name: name, fnc: reflect.ValueOf(f)} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + tr.typ = ti + } + return tr +} + +type transformer struct { + core + name string + typ reflect.Type // T + fnc reflect.Value // func(T) R +} + +func (tr *transformer) isFiltered() bool { return tr.typ != nil } + +func (tr *transformer) filter(s *state, t reflect.Type, _, _ reflect.Value) applicableOption { + for i := len(s.curPath) - 1; i >= 0; i-- { + if t, ok := s.curPath[i].(Transform); !ok { + break // Hit most recent non-Transform step + } else if tr == t.trans { + return nil // Cannot directly use same Transform + } + } + if tr.typ == nil || t.AssignableTo(tr.typ) { + return tr + } + return nil +} + +func (tr *transformer) apply(s *state, vx, vy reflect.Value) { + step := Transform{&transform{pathStep{typ: tr.fnc.Type().Out(0)}, tr}} + vvx := s.callTRFunc(tr.fnc, vx, step) + vvy := s.callTRFunc(tr.fnc, vy, step) + step.vx, step.vy = vvx, vvy + s.compareAny(step) +} + +func (tr transformer) String() string { + return fmt.Sprintf("Transformer(%s, %s)", tr.name, function.NameOf(tr.fnc)) +} + +// Comparer returns an Option that determines whether two values are equal +// to each other. +// +// The comparer f must be a function "func(T, T) bool" and is implicitly +// filtered to input values assignable to T. If T is an interface, it is +// possible that f is called with two values of different concrete types that +// both implement T. +// +// The equality function must be: +// • Symmetric: equal(x, y) == equal(y, x) +// • Deterministic: equal(x, y) == equal(x, y) +// • Pure: equal(x, y) does not modify x or y +func Comparer(f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Equal) || v.IsNil() { + panic(fmt.Sprintf("invalid comparer function: %T", f)) + } + cm := &comparer{fnc: v} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + cm.typ = ti + } + return cm +} + +type comparer struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (cm *comparer) isFiltered() bool { return cm.typ != nil } + +func (cm *comparer) filter(_ *state, t reflect.Type, _, _ reflect.Value) applicableOption { + if cm.typ == nil || t.AssignableTo(cm.typ) { + return cm + } + return nil +} + +func (cm *comparer) apply(s *state, vx, vy reflect.Value) { + eq := s.callTTBFunc(cm.fnc, vx, vy) + s.report(eq, reportByFunc) +} + +func (cm comparer) String() string { + return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc)) +} + +// Exporter returns an Option that specifies whether Equal is allowed to +// introspect into the unexported fields of certain struct types. +// +// Users of this option must understand that comparing on unexported fields +// from external packages is not safe since changes in the internal +// implementation of some external package may cause the result of Equal +// to unexpectedly change. However, it may be valid to use this option on types +// defined in an internal package where the semantic meaning of an unexported +// field is in the control of the user. +// +// In many cases, a custom Comparer should be used instead that defines +// equality as a function of the public API of a type rather than the underlying +// unexported implementation. +// +// For example, the reflect.Type documentation defines equality to be determined +// by the == operator on the interface (essentially performing a shallow pointer +// comparison) and most attempts to compare *regexp.Regexp types are interested +// in only checking that the regular expression strings are equal. +// Both of these are accomplished using Comparers: +// +// Comparer(func(x, y reflect.Type) bool { return x == y }) +// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() }) +// +// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore +// all unexported fields on specified struct types. +func Exporter(f func(reflect.Type) bool) Option { + if !supportExporters { + panic("Exporter is not supported on purego builds") + } + return exporter(f) +} + +type exporter func(reflect.Type) bool + +func (exporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + panic("not implemented") +} + +// AllowUnexported returns an Options that allows Equal to forcibly introspect +// unexported fields of the specified struct types. +// +// See Exporter for the proper use of this option. +func AllowUnexported(types ...interface{}) Option { + m := make(map[reflect.Type]bool) + for _, typ := range types { + t := reflect.TypeOf(typ) + if t.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid struct type: %T", typ)) + } + m[t] = true + } + return exporter(func(t reflect.Type) bool { return m[t] }) +} + +// Result represents the comparison result for a single node and +// is provided by cmp when calling Result (see Reporter). +type Result struct { + _ [0]func() // Make Result incomparable + flags resultFlags +} + +// Equal reports whether the node was determined to be equal or not. +// As a special case, ignored nodes are considered equal. +func (r Result) Equal() bool { + return r.flags&(reportEqual|reportByIgnore) != 0 +} + +// ByIgnore reports whether the node is equal because it was ignored. +// This never reports true if Equal reports false. +func (r Result) ByIgnore() bool { + return r.flags&reportByIgnore != 0 +} + +// ByMethod reports whether the Equal method determined equality. +func (r Result) ByMethod() bool { + return r.flags&reportByMethod != 0 +} + +// ByFunc reports whether a Comparer function determined equality. +func (r Result) ByFunc() bool { + return r.flags&reportByFunc != 0 +} + +// ByCycle reports whether a reference cycle was detected. +func (r Result) ByCycle() bool { + return r.flags&reportByCycle != 0 +} + +type resultFlags uint + +const ( + _ resultFlags = (1 << iota) / 2 + + reportEqual + reportUnequal + reportByIgnore + reportByMethod + reportByFunc + reportByCycle +) + +// Reporter is an Option that can be passed to Equal. When Equal traverses +// the value trees, it calls PushStep as it descends into each node in the +// tree and PopStep as it ascend out of the node. The leaves of the tree are +// either compared (determined to be equal or not equal) or ignored and reported +// as such by calling the Report method. +func Reporter(r interface { + // PushStep is called when a tree-traversal operation is performed. + // The PathStep itself is only valid until the step is popped. + // The PathStep.Values are valid for the duration of the entire traversal + // and must not be mutated. + // + // Equal always calls PushStep at the start to provide an operation-less + // PathStep used to report the root values. + // + // Within a slice, the exact set of inserted, removed, or modified elements + // is unspecified and may change in future implementations. + // The entries of a map are iterated through in an unspecified order. + PushStep(PathStep) + + // Report is called exactly once on leaf nodes to report whether the + // comparison identified the node as equal, unequal, or ignored. + // A leaf node is one that is immediately preceded by and followed by + // a pair of PushStep and PopStep calls. + Report(Result) + + // PopStep ascends back up the value tree. + // There is always a matching pop call for every push call. + PopStep() +}) Option { + return reporter{r} +} + +type reporter struct{ reporterIface } +type reporterIface interface { + PushStep(PathStep) + Report(Result) + PopStep() +} + +func (reporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + panic("not implemented") +} + +// normalizeOption normalizes the input options such that all Options groups +// are flattened and groups with a single element are reduced to that element. +// Only coreOptions and Options containing coreOptions are allowed. +func normalizeOption(src Option) Option { + switch opts := flattenOptions(nil, Options{src}); len(opts) { + case 0: + return nil + case 1: + return opts[0] + default: + return opts + } +} + +// flattenOptions copies all options in src to dst as a flat list. +// Only coreOptions and Options containing coreOptions are allowed. +func flattenOptions(dst, src Options) Options { + for _, opt := range src { + switch opt := opt.(type) { + case nil: + continue + case Options: + dst = flattenOptions(dst, opt) + case coreOption: + dst = append(dst, opt) + default: + panic(fmt.Sprintf("invalid option type: %T", opt)) + } + } + return dst +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/options_test.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/options_test.go new file mode 100644 index 0000000..c7d45f3 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/options_test.go @@ -0,0 +1,216 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "io" + "reflect" + "strings" + "testing" + + ts "github.com/google/go-cmp/cmp/internal/teststructs" +) + +// Test that the creation of Option values with non-sensible inputs produces +// a run-time panic with a decent error message +func TestOptionPanic(t *testing.T) { + type myBool bool + tests := []struct { + label string // Test description + fnc interface{} // Option function to call + args []interface{} // Arguments to pass in + wantPanic string // Expected panic message + }{{ + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{1}, + wantPanic: "invalid struct type", + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}, ts.StructB{}, ts.StructA{}}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}, &ts.StructB{}, ts.StructA{}}, + wantPanic: "invalid struct type", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{5}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y interface{}) bool { return true }}, + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y io.Reader) bool { return true }}, + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y io.Reader) myBool { return true }}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x string, y interface{}) bool { return true }}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{(func(int, int) bool)(nil)}, + wantPanic: "invalid comparer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", 0}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int) int { return 0 }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(bool) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int, int) bool { return true }}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", (func(int) uint)(nil)}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"Func", func(Path) Path { return nil }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"世界", func(int) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"/*", func(int) bool { return true }}, + wantPanic: "invalid name", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"_", func(int) bool { return true }}, + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{(func(Path) bool)(nil), Ignore()}, + wantPanic: "invalid path filter function", + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Ignore()}, + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Reporter(&defaultReporter{})}, + wantPanic: "invalid option type", + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), Ignore()}}, + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}}, + wantPanic: "invalid option type", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{0, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y int) bool { return true }, Ignore()}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y interface{}) bool { return true }, Ignore()}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y interface{}) myBool { return true }, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x io.Reader, y interface{}) bool { return true }, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{(func(int, int) bool)(nil), Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, Reporter(&defaultReporter{})}, + wantPanic: "invalid option type", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), Ignore()}}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}}, + wantPanic: "invalid option type", + }} + + for _, tt := range tests { + t.Run(tt.label, func(t *testing.T) { + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + var vargs []reflect.Value + for _, arg := range tt.args { + vargs = append(vargs, reflect.ValueOf(arg)) + } + reflect.ValueOf(tt.fnc).Call(vargs) + }() + if tt.wantPanic == "" { + if gotPanic != "" { + t.Fatalf("unexpected panic message: %s", gotPanic) + } + } else { + if !strings.Contains(gotPanic, tt.wantPanic) { + t.Fatalf("panic message:\ngot: %s\nwant: %s", gotPanic, tt.wantPanic) + } + } + }) + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/path.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/path.go new file mode 100644 index 0000000..3d45c1a --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/path.go @@ -0,0 +1,378 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/value" +) + +// Path is a list of PathSteps describing the sequence of operations to get +// from some root type to the current position in the value tree. +// The first Path element is always an operation-less PathStep that exists +// simply to identify the initial type. +// +// When traversing structs with embedded structs, the embedded struct will +// always be accessed as a field before traversing the fields of the +// embedded struct themselves. That is, an exported field from the +// embedded struct will never be accessed directly from the parent struct. +type Path []PathStep + +// PathStep is a union-type for specific operations to traverse +// a value's tree structure. Users of this package never need to implement +// these types as values of this type will be returned by this package. +// +// Implementations of this interface are +// StructField, SliceIndex, MapIndex, Indirect, TypeAssertion, and Transform. +type PathStep interface { + String() string + + // Type is the resulting type after performing the path step. + Type() reflect.Type + + // Values is the resulting values after performing the path step. + // The type of each valid value is guaranteed to be identical to Type. + // + // In some cases, one or both may be invalid or have restrictions: + // • For StructField, both are not interface-able if the current field + // is unexported and the struct type is not explicitly permitted by + // an Exporter to traverse unexported fields. + // • For SliceIndex, one may be invalid if an element is missing from + // either the x or y slice. + // • For MapIndex, one may be invalid if an entry is missing from + // either the x or y map. + // + // The provided values must not be mutated. + Values() (vx, vy reflect.Value) +} + +var ( + _ PathStep = StructField{} + _ PathStep = SliceIndex{} + _ PathStep = MapIndex{} + _ PathStep = Indirect{} + _ PathStep = TypeAssertion{} + _ PathStep = Transform{} +) + +func (pa *Path) push(s PathStep) { + *pa = append(*pa, s) +} + +func (pa *Path) pop() { + *pa = (*pa)[:len(*pa)-1] +} + +// Last returns the last PathStep in the Path. +// If the path is empty, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Last() PathStep { + return pa.Index(-1) +} + +// Index returns the ith step in the Path and supports negative indexing. +// A negative index starts counting from the tail of the Path such that -1 +// refers to the last step, -2 refers to the second-to-last step, and so on. +// If index is invalid, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Index(i int) PathStep { + if i < 0 { + i = len(pa) + i + } + if i < 0 || i >= len(pa) { + return pathStep{} + } + return pa[i] +} + +// String returns the simplified path to a node. +// The simplified path only contains struct field accesses. +// +// For example: +// MyMap.MySlices.MyField +func (pa Path) String() string { + var ss []string + for _, s := range pa { + if _, ok := s.(StructField); ok { + ss = append(ss, s.String()) + } + } + return strings.TrimPrefix(strings.Join(ss, ""), ".") +} + +// GoString returns the path to a specific node using Go syntax. +// +// For example: +// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField +func (pa Path) GoString() string { + var ssPre, ssPost []string + var numIndirect int + for i, s := range pa { + var nextStep PathStep + if i+1 < len(pa) { + nextStep = pa[i+1] + } + switch s := s.(type) { + case Indirect: + numIndirect++ + pPre, pPost := "(", ")" + switch nextStep.(type) { + case Indirect: + continue // Next step is indirection, so let them batch up + case StructField: + numIndirect-- // Automatic indirection on struct fields + case nil: + pPre, pPost = "", "" // Last step; no need for parenthesis + } + if numIndirect > 0 { + ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect)) + ssPost = append(ssPost, pPost) + } + numIndirect = 0 + continue + case Transform: + ssPre = append(ssPre, s.trans.name+"(") + ssPost = append(ssPost, ")") + continue + } + ssPost = append(ssPost, s.String()) + } + for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 { + ssPre[i], ssPre[j] = ssPre[j], ssPre[i] + } + return strings.Join(ssPre, "") + strings.Join(ssPost, "") +} + +type pathStep struct { + typ reflect.Type + vx, vy reflect.Value +} + +func (ps pathStep) Type() reflect.Type { return ps.typ } +func (ps pathStep) Values() (vx, vy reflect.Value) { return ps.vx, ps.vy } +func (ps pathStep) String() string { + if ps.typ == nil { + return "" + } + s := ps.typ.String() + if s == "" || strings.ContainsAny(s, "{}\n") { + return "root" // Type too simple or complex to print + } + return fmt.Sprintf("{%s}", s) +} + +// StructField represents a struct field access on a field called Name. +type StructField struct{ *structField } +type structField struct { + pathStep + name string + idx int + + // These fields are used for forcibly accessing an unexported field. + // pvx, pvy, and field are only valid if unexported is true. + unexported bool + mayForce bool // Forcibly allow visibility + paddr bool // Was parent addressable? + pvx, pvy reflect.Value // Parent values (always addressible) + field reflect.StructField // Field information +} + +func (sf StructField) Type() reflect.Type { return sf.typ } +func (sf StructField) Values() (vx, vy reflect.Value) { + if !sf.unexported { + return sf.vx, sf.vy // CanInterface reports true + } + + // Forcibly obtain read-write access to an unexported struct field. + if sf.mayForce { + vx = retrieveUnexportedField(sf.pvx, sf.field, sf.paddr) + vy = retrieveUnexportedField(sf.pvy, sf.field, sf.paddr) + return vx, vy // CanInterface reports true + } + return sf.vx, sf.vy // CanInterface reports false +} +func (sf StructField) String() string { return fmt.Sprintf(".%s", sf.name) } + +// Name is the field name. +func (sf StructField) Name() string { return sf.name } + +// Index is the index of the field in the parent struct type. +// See reflect.Type.Field. +func (sf StructField) Index() int { return sf.idx } + +// SliceIndex is an index operation on a slice or array at some index Key. +type SliceIndex struct{ *sliceIndex } +type sliceIndex struct { + pathStep + xkey, ykey int + isSlice bool // False for reflect.Array +} + +func (si SliceIndex) Type() reflect.Type { return si.typ } +func (si SliceIndex) Values() (vx, vy reflect.Value) { return si.vx, si.vy } +func (si SliceIndex) String() string { + switch { + case si.xkey == si.ykey: + return fmt.Sprintf("[%d]", si.xkey) + case si.ykey == -1: + // [5->?] means "I don't know where X[5] went" + return fmt.Sprintf("[%d->?]", si.xkey) + case si.xkey == -1: + // [?->3] means "I don't know where Y[3] came from" + return fmt.Sprintf("[?->%d]", si.ykey) + default: + // [5->3] means "X[5] moved to Y[3]" + return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) + } +} + +// Key is the index key; it may return -1 if in a split state +func (si SliceIndex) Key() int { + if si.xkey != si.ykey { + return -1 + } + return si.xkey +} + +// SplitKeys are the indexes for indexing into slices in the +// x and y values, respectively. These indexes may differ due to the +// insertion or removal of an element in one of the slices, causing +// all of the indexes to be shifted. If an index is -1, then that +// indicates that the element does not exist in the associated slice. +// +// Key is guaranteed to return -1 if and only if the indexes returned +// by SplitKeys are not the same. SplitKeys will never return -1 for +// both indexes. +func (si SliceIndex) SplitKeys() (ix, iy int) { return si.xkey, si.ykey } + +// MapIndex is an index operation on a map at some index Key. +type MapIndex struct{ *mapIndex } +type mapIndex struct { + pathStep + key reflect.Value +} + +func (mi MapIndex) Type() reflect.Type { return mi.typ } +func (mi MapIndex) Values() (vx, vy reflect.Value) { return mi.vx, mi.vy } +func (mi MapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } + +// Key is the value of the map key. +func (mi MapIndex) Key() reflect.Value { return mi.key } + +// Indirect represents pointer indirection on the parent type. +type Indirect struct{ *indirect } +type indirect struct { + pathStep +} + +func (in Indirect) Type() reflect.Type { return in.typ } +func (in Indirect) Values() (vx, vy reflect.Value) { return in.vx, in.vy } +func (in Indirect) String() string { return "*" } + +// TypeAssertion represents a type assertion on an interface. +type TypeAssertion struct{ *typeAssertion } +type typeAssertion struct { + pathStep +} + +func (ta TypeAssertion) Type() reflect.Type { return ta.typ } +func (ta TypeAssertion) Values() (vx, vy reflect.Value) { return ta.vx, ta.vy } +func (ta TypeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) } + +// Transform is a transformation from the parent type to the current type. +type Transform struct{ *transform } +type transform struct { + pathStep + trans *transformer +} + +func (tf Transform) Type() reflect.Type { return tf.typ } +func (tf Transform) Values() (vx, vy reflect.Value) { return tf.vx, tf.vy } +func (tf Transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } + +// Name is the name of the Transformer. +func (tf Transform) Name() string { return tf.trans.name } + +// Func is the function pointer to the transformer function. +func (tf Transform) Func() reflect.Value { return tf.trans.fnc } + +// Option returns the originally constructed Transformer option. +// The == operator can be used to detect the exact option used. +func (tf Transform) Option() Option { return tf.trans } + +// pointerPath represents a dual-stack of pointers encountered when +// recursively traversing the x and y values. This data structure supports +// detection of cycles and determining whether the cycles are equal. +// In Go, cycles can occur via pointers, slices, and maps. +// +// The pointerPath uses a map to represent a stack; where descension into a +// pointer pushes the address onto the stack, and ascension from a pointer +// pops the address from the stack. Thus, when traversing into a pointer from +// reflect.Ptr, reflect.Slice element, or reflect.Map, we can detect cycles +// by checking whether the pointer has already been visited. The cycle detection +// uses a seperate stack for the x and y values. +// +// If a cycle is detected we need to determine whether the two pointers +// should be considered equal. The definition of equality chosen by Equal +// requires two graphs to have the same structure. To determine this, both the +// x and y values must have a cycle where the previous pointers were also +// encountered together as a pair. +// +// Semantically, this is equivalent to augmenting Indirect, SliceIndex, and +// MapIndex with pointer information for the x and y values. +// Suppose px and py are two pointers to compare, we then search the +// Path for whether px was ever encountered in the Path history of x, and +// similarly so with py. If either side has a cycle, the comparison is only +// equal if both px and py have a cycle resulting from the same PathStep. +// +// Using a map as a stack is more performant as we can perform cycle detection +// in O(1) instead of O(N) where N is len(Path). +type pointerPath struct { + // mx is keyed by x pointers, where the value is the associated y pointer. + mx map[value.Pointer]value.Pointer + // my is keyed by y pointers, where the value is the associated x pointer. + my map[value.Pointer]value.Pointer +} + +func (p *pointerPath) Init() { + p.mx = make(map[value.Pointer]value.Pointer) + p.my = make(map[value.Pointer]value.Pointer) +} + +// Push indicates intent to descend into pointers vx and vy where +// visited reports whether either has been seen before. If visited before, +// equal reports whether both pointers were encountered together. +// Pop must be called if and only if the pointers were never visited. +// +// The pointers vx and vy must be a reflect.Ptr, reflect.Slice, or reflect.Map +// and be non-nil. +func (p pointerPath) Push(vx, vy reflect.Value) (equal, visited bool) { + px := value.PointerOf(vx) + py := value.PointerOf(vy) + _, ok1 := p.mx[px] + _, ok2 := p.my[py] + if ok1 || ok2 { + equal = p.mx[px] == py && p.my[py] == px // Pointers paired together + return equal, true + } + p.mx[px] = py + p.my[py] = px + return false, false +} + +// Pop ascends from pointers vx and vy. +func (p pointerPath) Pop(vx, vy reflect.Value) { + delete(p.mx, value.PointerOf(vx)) + delete(p.my, value.PointerOf(vy)) +} + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report.go new file mode 100644 index 0000000..f43cd12 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report.go @@ -0,0 +1,54 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +// defaultReporter implements the reporter interface. +// +// As Equal serially calls the PushStep, Report, and PopStep methods, the +// defaultReporter constructs a tree-based representation of the compared value +// and the result of each comparison (see valueNode). +// +// When the String method is called, the FormatDiff method transforms the +// valueNode tree into a textNode tree, which is a tree-based representation +// of the textual output (see textNode). +// +// Lastly, the textNode.String method produces the final report as a string. +type defaultReporter struct { + root *valueNode + curr *valueNode +} + +func (r *defaultReporter) PushStep(ps PathStep) { + r.curr = r.curr.PushStep(ps) + if r.root == nil { + r.root = r.curr + } +} +func (r *defaultReporter) Report(rs Result) { + r.curr.Report(rs) +} +func (r *defaultReporter) PopStep() { + r.curr = r.curr.PopStep() +} + +// String provides a full report of the differences detected as a structured +// literal in pseudo-Go syntax. String may only be called after the entire tree +// has been traversed. +func (r *defaultReporter) String() string { + assert(r.root != nil && r.curr == nil) + if r.root.NumDiff == 0 { + return "" + } + ptrs := new(pointerReferences) + text := formatOptions{}.FormatDiff(r.root, ptrs) + resolveReferences(text) + return text.String() +} + +func assert(ok bool) { + if !ok { + panic("assertion failure") + } +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_compare.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_compare.go new file mode 100644 index 0000000..104bb30 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_compare.go @@ -0,0 +1,432 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "fmt" + "reflect" + + "github.com/google/go-cmp/cmp/internal/value" +) + +// numContextRecords is the number of surrounding equal records to print. +const numContextRecords = 2 + +type diffMode byte + +const ( + diffUnknown diffMode = 0 + diffIdentical diffMode = ' ' + diffRemoved diffMode = '-' + diffInserted diffMode = '+' +) + +type typeMode int + +const ( + // emitType always prints the type. + emitType typeMode = iota + // elideType never prints the type. + elideType + // autoType prints the type only for composite kinds + // (i.e., structs, slices, arrays, and maps). + autoType +) + +type formatOptions struct { + // DiffMode controls the output mode of FormatDiff. + // + // If diffUnknown, then produce a diff of the x and y values. + // If diffIdentical, then emit values as if they were equal. + // If diffRemoved, then only emit x values (ignoring y values). + // If diffInserted, then only emit y values (ignoring x values). + DiffMode diffMode + + // TypeMode controls whether to print the type for the current node. + // + // As a general rule of thumb, we always print the type of the next node + // after an interface, and always elide the type of the next node after + // a slice or map node. + TypeMode typeMode + + // formatValueOptions are options specific to printing reflect.Values. + formatValueOptions +} + +func (opts formatOptions) WithDiffMode(d diffMode) formatOptions { + opts.DiffMode = d + return opts +} +func (opts formatOptions) WithTypeMode(t typeMode) formatOptions { + opts.TypeMode = t + return opts +} +func (opts formatOptions) WithVerbosity(level int) formatOptions { + opts.VerbosityLevel = level + opts.LimitVerbosity = true + return opts +} +func (opts formatOptions) verbosity() uint { + switch { + case opts.VerbosityLevel < 0: + return 0 + case opts.VerbosityLevel > 16: + return 16 // some reasonable maximum to avoid shift overflow + default: + return uint(opts.VerbosityLevel) + } +} + +const maxVerbosityPreset = 6 + +// verbosityPreset modifies the verbosity settings given an index +// between 0 and maxVerbosityPreset, inclusive. +func verbosityPreset(opts formatOptions, i int) formatOptions { + opts.VerbosityLevel = int(opts.verbosity()) + 2*i + if i > 0 { + opts.AvoidStringer = true + } + if i >= maxVerbosityPreset { + opts.PrintAddresses = true + opts.QualifiedNames = true + } + return opts +} + +// FormatDiff converts a valueNode tree into a textNode tree, where the later +// is a textual representation of the differences detected in the former. +func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) { + if opts.DiffMode == diffIdentical { + opts = opts.WithVerbosity(1) + } else if opts.verbosity() < 3 { + opts = opts.WithVerbosity(3) + } + + // Check whether we have specialized formatting for this node. + // This is not necessary, but helpful for producing more readable outputs. + if opts.CanFormatDiffSlice(v) { + return opts.FormatDiffSlice(v) + } + + var parentKind reflect.Kind + if v.parent != nil && v.parent.TransformerName == "" { + parentKind = v.parent.Type.Kind() + } + + // For leaf nodes, format the value based on the reflect.Values alone. + if v.MaxDepth == 0 { + switch opts.DiffMode { + case diffUnknown, diffIdentical: + // Format Equal. + if v.NumDiff == 0 { + outx := opts.FormatValue(v.ValueX, parentKind, ptrs) + outy := opts.FormatValue(v.ValueY, parentKind, ptrs) + if v.NumIgnored > 0 && v.NumSame == 0 { + return textEllipsis + } else if outx.Len() < outy.Len() { + return outx + } else { + return outy + } + } + + // Format unequal. + assert(opts.DiffMode == diffUnknown) + var list textList + outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs) + outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs) + for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { + opts2 := verbosityPreset(opts, i).WithTypeMode(elideType) + outx = opts2.FormatValue(v.ValueX, parentKind, ptrs) + outy = opts2.FormatValue(v.ValueY, parentKind, ptrs) + } + if outx != nil { + list = append(list, textRecord{Diff: '-', Value: outx}) + } + if outy != nil { + list = append(list, textRecord{Diff: '+', Value: outy}) + } + return opts.WithTypeMode(emitType).FormatType(v.Type, list) + case diffRemoved: + return opts.FormatValue(v.ValueX, parentKind, ptrs) + case diffInserted: + return opts.FormatValue(v.ValueY, parentKind, ptrs) + default: + panic("invalid diff mode") + } + } + + // Register slice element to support cycle detection. + if parentKind == reflect.Slice { + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true) + defer ptrs.Pop() + defer func() { out = wrapTrunkReferences(ptrRefs, out) }() + } + + // Descend into the child value node. + if v.TransformerName != "" { + out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) + out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"} + return opts.FormatType(v.Type, out) + } else { + switch k := v.Type.Kind(); k { + case reflect.Struct, reflect.Array, reflect.Slice: + out = opts.formatDiffList(v.Records, k, ptrs) + out = opts.FormatType(v.Type, out) + case reflect.Map: + // Register map to support cycle detection. + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) + defer ptrs.Pop() + + out = opts.formatDiffList(v.Records, k, ptrs) + out = wrapTrunkReferences(ptrRefs, out) + out = opts.FormatType(v.Type, out) + case reflect.Ptr: + // Register pointer to support cycle detection. + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) + defer ptrs.Pop() + + out = opts.FormatDiff(v.Value, ptrs) + out = wrapTrunkReferences(ptrRefs, out) + out = &textWrap{Prefix: "&", Value: out} + case reflect.Interface: + out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) + default: + panic(fmt.Sprintf("%v cannot have children", k)) + } + return out + } +} + +func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode { + // Derive record name based on the data structure kind. + var name string + var formatKey func(reflect.Value) string + switch k { + case reflect.Struct: + name = "field" + opts = opts.WithTypeMode(autoType) + formatKey = func(v reflect.Value) string { return v.String() } + case reflect.Slice, reflect.Array: + name = "element" + opts = opts.WithTypeMode(elideType) + formatKey = func(reflect.Value) string { return "" } + case reflect.Map: + name = "entry" + opts = opts.WithTypeMode(elideType) + formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) } + } + + maxLen := -1 + if opts.LimitVerbosity { + if opts.DiffMode == diffIdentical { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + } else { + maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc... + } + opts.VerbosityLevel-- + } + + // Handle unification. + switch opts.DiffMode { + case diffIdentical, diffRemoved, diffInserted: + var list textList + var deferredEllipsis bool // Add final "..." to indicate records were dropped + for _, r := range recs { + if len(list) == maxLen { + deferredEllipsis = true + break + } + + // Elide struct fields that are zero value. + if k == reflect.Struct { + var isZero bool + switch opts.DiffMode { + case diffIdentical: + isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY) + case diffRemoved: + isZero = value.IsZero(r.Value.ValueX) + case diffInserted: + isZero = value.IsZero(r.Value.ValueY) + } + if isZero { + continue + } + } + // Elide ignored nodes. + if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 { + deferredEllipsis = !(k == reflect.Slice || k == reflect.Array) + if !deferredEllipsis { + list.AppendEllipsis(diffStats{}) + } + continue + } + if out := opts.FormatDiff(r.Value, ptrs); out != nil { + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + } + } + if deferredEllipsis { + list.AppendEllipsis(diffStats{}) + } + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} + case diffUnknown: + default: + panic("invalid diff mode") + } + + // Handle differencing. + var numDiffs int + var list textList + var keys []reflect.Value // invariant: len(list) == len(keys) + groups := coalesceAdjacentRecords(name, recs) + maxGroup := diffStats{Name: name} + for i, ds := range groups { + if maxLen >= 0 && numDiffs >= maxLen { + maxGroup = maxGroup.Append(ds) + continue + } + + // Handle equal records. + if ds.NumDiff() == 0 { + // Compute the number of leading and trailing records to print. + var numLo, numHi int + numEqual := ds.NumIgnored + ds.NumIdentical + for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 { + if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { + break + } + numLo++ + } + for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { + if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { + break + } + numHi++ + } + if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 { + numHi++ // Avoid pointless coalescing of a single equal record + } + + // Format the equal values. + for _, r := range recs[:numLo] { + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + if numEqual > numLo+numHi { + ds.NumIdentical -= numLo + numHi + list.AppendEllipsis(ds) + for len(keys) < len(list) { + keys = append(keys, reflect.Value{}) + } + } + for _, r := range recs[numEqual-numHi : numEqual] { + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + recs = recs[numEqual:] + continue + } + + // Handle unequal records. + for _, r := range recs[:ds.NumDiff()] { + switch { + case opts.CanFormatDiffSlice(r.Value): + out := opts.FormatDiffSlice(r.Value) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + case r.Value.NumChildren == r.Value.MaxDepth: + outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) + outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) + for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { + opts2 := verbosityPreset(opts, i) + outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) + outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) + } + if outx != nil { + list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) + keys = append(keys, r.Key) + } + if outy != nil { + list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy}) + keys = append(keys, r.Key) + } + default: + out := opts.FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + } + recs = recs[ds.NumDiff():] + numDiffs += ds.NumDiff() + } + if maxGroup.IsZero() { + assert(len(recs) == 0) + } else { + list.AppendEllipsis(maxGroup) + for len(keys) < len(list) { + keys = append(keys, reflect.Value{}) + } + } + assert(len(list) == len(keys)) + + // For maps, the default formatting logic uses fmt.Stringer which may + // produce ambiguous output. Avoid calling String to disambiguate. + if k == reflect.Map { + var ambiguous bool + seenKeys := map[string]reflect.Value{} + for i, currKey := range keys { + if currKey.IsValid() { + strKey := list[i].Key + prevKey, seen := seenKeys[strKey] + if seen && prevKey.CanInterface() && currKey.CanInterface() { + ambiguous = prevKey.Interface() != currKey.Interface() + if ambiguous { + break + } + } + seenKeys[strKey] = currKey + } + } + if ambiguous { + for i, k := range keys { + if k.IsValid() { + list[i].Key = formatMapKey(k, true, ptrs) + } + } + } + } + + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} +} + +// coalesceAdjacentRecords coalesces the list of records into groups of +// adjacent equal, or unequal counts. +func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) { + var prevCase int // Arbitrary index into which case last occurred + lastStats := func(i int) *diffStats { + if prevCase != i { + groups = append(groups, diffStats{Name: name}) + prevCase = i + } + return &groups[len(groups)-1] + } + for _, r := range recs { + switch rv := r.Value; { + case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0: + lastStats(1).NumIgnored++ + case rv.NumDiff == 0: + lastStats(1).NumIdentical++ + case rv.NumDiff > 0 && !rv.ValueY.IsValid(): + lastStats(2).NumRemoved++ + case rv.NumDiff > 0 && !rv.ValueX.IsValid(): + lastStats(2).NumInserted++ + default: + lastStats(2).NumModified++ + } + } + return groups +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_references.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_references.go new file mode 100644 index 0000000..be31b33 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_references.go @@ -0,0 +1,264 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/flags" + "github.com/google/go-cmp/cmp/internal/value" +) + +const ( + pointerDelimPrefix = "⟪" + pointerDelimSuffix = "⟫" +) + +// formatPointer prints the address of the pointer. +func formatPointer(p value.Pointer, withDelims bool) string { + v := p.Uintptr() + if flags.Deterministic { + v = 0xdeadf00f // Only used for stable testing purposes + } + if withDelims { + return pointerDelimPrefix + formatHex(uint64(v)) + pointerDelimSuffix + } + return formatHex(uint64(v)) +} + +// pointerReferences is a stack of pointers visited so far. +type pointerReferences [][2]value.Pointer + +func (ps *pointerReferences) PushPair(vx, vy reflect.Value, d diffMode, deref bool) (pp [2]value.Pointer) { + if deref && vx.IsValid() { + vx = vx.Addr() + } + if deref && vy.IsValid() { + vy = vy.Addr() + } + switch d { + case diffUnknown, diffIdentical: + pp = [2]value.Pointer{value.PointerOf(vx), value.PointerOf(vy)} + case diffRemoved: + pp = [2]value.Pointer{value.PointerOf(vx), value.Pointer{}} + case diffInserted: + pp = [2]value.Pointer{value.Pointer{}, value.PointerOf(vy)} + } + *ps = append(*ps, pp) + return pp +} + +func (ps *pointerReferences) Push(v reflect.Value) (p value.Pointer, seen bool) { + p = value.PointerOf(v) + for _, pp := range *ps { + if p == pp[0] || p == pp[1] { + return p, true + } + } + *ps = append(*ps, [2]value.Pointer{p, p}) + return p, false +} + +func (ps *pointerReferences) Pop() { + *ps = (*ps)[:len(*ps)-1] +} + +// trunkReferences is metadata for a textNode indicating that the sub-tree +// represents the value for either pointer in a pair of references. +type trunkReferences struct{ pp [2]value.Pointer } + +// trunkReference is metadata for a textNode indicating that the sub-tree +// represents the value for the given pointer reference. +type trunkReference struct{ p value.Pointer } + +// leafReference is metadata for a textNode indicating that the value is +// truncated as it refers to another part of the tree (i.e., a trunk). +type leafReference struct{ p value.Pointer } + +func wrapTrunkReferences(pp [2]value.Pointer, s textNode) textNode { + switch { + case pp[0].IsNil(): + return &textWrap{Value: s, Metadata: trunkReference{pp[1]}} + case pp[1].IsNil(): + return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} + case pp[0] == pp[1]: + return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} + default: + return &textWrap{Value: s, Metadata: trunkReferences{pp}} + } +} +func wrapTrunkReference(p value.Pointer, printAddress bool, s textNode) textNode { + var prefix string + if printAddress { + prefix = formatPointer(p, true) + } + return &textWrap{Prefix: prefix, Value: s, Metadata: trunkReference{p}} +} +func makeLeafReference(p value.Pointer, printAddress bool) textNode { + out := &textWrap{Prefix: "(", Value: textEllipsis, Suffix: ")"} + var prefix string + if printAddress { + prefix = formatPointer(p, true) + } + return &textWrap{Prefix: prefix, Value: out, Metadata: leafReference{p}} +} + +// resolveReferences walks the textNode tree searching for any leaf reference +// metadata and resolves each against the corresponding trunk references. +// Since pointer addresses in memory are not particularly readable to the user, +// it replaces each pointer value with an arbitrary and unique reference ID. +func resolveReferences(s textNode) { + var walkNodes func(textNode, func(textNode)) + walkNodes = func(s textNode, f func(textNode)) { + f(s) + switch s := s.(type) { + case *textWrap: + walkNodes(s.Value, f) + case textList: + for _, r := range s { + walkNodes(r.Value, f) + } + } + } + + // Collect all trunks and leaves with reference metadata. + var trunks, leaves []*textWrap + walkNodes(s, func(s textNode) { + if s, ok := s.(*textWrap); ok { + switch s.Metadata.(type) { + case leafReference: + leaves = append(leaves, s) + case trunkReference, trunkReferences: + trunks = append(trunks, s) + } + } + }) + + // No leaf references to resolve. + if len(leaves) == 0 { + return + } + + // Collect the set of all leaf references to resolve. + leafPtrs := make(map[value.Pointer]bool) + for _, leaf := range leaves { + leafPtrs[leaf.Metadata.(leafReference).p] = true + } + + // Collect the set of trunk pointers that are always paired together. + // This allows us to assign a single ID to both pointers for brevity. + // If a pointer in a pair ever occurs by itself or as a different pair, + // then the pair is broken. + pairedTrunkPtrs := make(map[value.Pointer]value.Pointer) + unpair := func(p value.Pointer) { + if !pairedTrunkPtrs[p].IsNil() { + pairedTrunkPtrs[pairedTrunkPtrs[p]] = value.Pointer{} // invalidate other half + } + pairedTrunkPtrs[p] = value.Pointer{} // invalidate this half + } + for _, trunk := range trunks { + switch p := trunk.Metadata.(type) { + case trunkReference: + unpair(p.p) // standalone pointer cannot be part of a pair + case trunkReferences: + p0, ok0 := pairedTrunkPtrs[p.pp[0]] + p1, ok1 := pairedTrunkPtrs[p.pp[1]] + switch { + case !ok0 && !ok1: + // Register the newly seen pair. + pairedTrunkPtrs[p.pp[0]] = p.pp[1] + pairedTrunkPtrs[p.pp[1]] = p.pp[0] + case ok0 && ok1 && p0 == p.pp[1] && p1 == p.pp[0]: + // Exact pair already seen; do nothing. + default: + // Pair conflicts with some other pair; break all pairs. + unpair(p.pp[0]) + unpair(p.pp[1]) + } + } + } + + // Correlate each pointer referenced by leaves to a unique identifier, + // and print the IDs for each trunk that matches those pointers. + var nextID uint + ptrIDs := make(map[value.Pointer]uint) + newID := func() uint { + id := nextID + nextID++ + return id + } + for _, trunk := range trunks { + switch p := trunk.Metadata.(type) { + case trunkReference: + if print := leafPtrs[p.p]; print { + id, ok := ptrIDs[p.p] + if !ok { + id = newID() + ptrIDs[p.p] = id + } + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) + } + case trunkReferences: + print0 := leafPtrs[p.pp[0]] + print1 := leafPtrs[p.pp[1]] + if print0 || print1 { + id0, ok0 := ptrIDs[p.pp[0]] + id1, ok1 := ptrIDs[p.pp[1]] + isPair := pairedTrunkPtrs[p.pp[0]] == p.pp[1] && pairedTrunkPtrs[p.pp[1]] == p.pp[0] + if isPair { + var id uint + assert(ok0 == ok1) // must be seen together or not at all + if ok0 { + assert(id0 == id1) // must have the same ID + id = id0 + } else { + id = newID() + ptrIDs[p.pp[0]] = id + ptrIDs[p.pp[1]] = id + } + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) + } else { + if print0 && !ok0 { + id0 = newID() + ptrIDs[p.pp[0]] = id0 + } + if print1 && !ok1 { + id1 = newID() + ptrIDs[p.pp[1]] = id1 + } + switch { + case print0 && print1: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)+","+formatReference(id1)) + case print0: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)) + case print1: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id1)) + } + } + } + } + } + + // Update all leaf references with the unique identifier. + for _, leaf := range leaves { + if id, ok := ptrIDs[leaf.Metadata.(leafReference).p]; ok { + leaf.Prefix = updateReferencePrefix(leaf.Prefix, formatReference(id)) + } + } +} + +func formatReference(id uint) string { + return fmt.Sprintf("ref#%d", id) +} + +func updateReferencePrefix(prefix, ref string) string { + if prefix == "" { + return pointerDelimPrefix + ref + pointerDelimSuffix + } + suffix := strings.TrimPrefix(prefix, pointerDelimPrefix) + return pointerDelimPrefix + ref + ": " + suffix +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_reflect.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_reflect.go new file mode 100644 index 0000000..33f0357 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_reflect.go @@ -0,0 +1,402 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/value" +) + +type formatValueOptions struct { + // AvoidStringer controls whether to avoid calling custom stringer + // methods like error.Error or fmt.Stringer.String. + AvoidStringer bool + + // PrintAddresses controls whether to print the address of all pointers, + // slice elements, and maps. + PrintAddresses bool + + // QualifiedNames controls whether FormatType uses the fully qualified name + // (including the full package path as opposed to just the package name). + QualifiedNames bool + + // VerbosityLevel controls the amount of output to produce. + // A higher value produces more output. A value of zero or lower produces + // no output (represented using an ellipsis). + // If LimitVerbosity is false, then the level is treated as infinite. + VerbosityLevel int + + // LimitVerbosity specifies that formatting should respect VerbosityLevel. + LimitVerbosity bool +} + +// FormatType prints the type as if it were wrapping s. +// This may return s as-is depending on the current type and TypeMode mode. +func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { + // Check whether to emit the type or not. + switch opts.TypeMode { + case autoType: + switch t.Kind() { + case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map: + if s.Equal(textNil) { + return s + } + default: + return s + } + if opts.DiffMode == diffIdentical { + return s // elide type for identical nodes + } + case elideType: + return s + } + + // Determine the type label, applying special handling for unnamed types. + typeName := value.TypeString(t, opts.QualifiedNames) + if t.Name() == "" { + // According to Go grammar, certain type literals contain symbols that + // do not strongly bind to the next lexicographical token (e.g., *T). + switch t.Kind() { + case reflect.Chan, reflect.Func, reflect.Ptr: + typeName = "(" + typeName + ")" + } + } + return &textWrap{Prefix: typeName, Value: wrapParens(s)} +} + +// wrapParens wraps s with a set of parenthesis, but avoids it if the +// wrapped node itself is already surrounded by a pair of parenthesis or braces. +// It handles unwrapping one level of pointer-reference nodes. +func wrapParens(s textNode) textNode { + var refNode *textWrap + if s2, ok := s.(*textWrap); ok { + // Unwrap a single pointer reference node. + switch s2.Metadata.(type) { + case leafReference, trunkReference, trunkReferences: + refNode = s2 + if s3, ok := refNode.Value.(*textWrap); ok { + s2 = s3 + } + } + + // Already has delimiters that make parenthesis unnecessary. + hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")") + hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}") + if hasParens || hasBraces { + return s + } + } + if refNode != nil { + refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"} + return s + } + return &textWrap{Prefix: "(", Value: s, Suffix: ")"} +} + +// FormatValue prints the reflect.Value, taking extra care to avoid descending +// into pointers already in ptrs. As pointers are visited, ptrs is also updated. +func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) { + if !v.IsValid() { + return nil + } + t := v.Type() + + // Check slice element for cycles. + if parentKind == reflect.Slice { + ptrRef, visited := ptrs.Push(v.Addr()) + if visited { + return makeLeafReference(ptrRef, false) + } + defer ptrs.Pop() + defer func() { out = wrapTrunkReference(ptrRef, false, out) }() + } + + // Check whether there is an Error or String method to call. + if !opts.AvoidStringer && v.CanInterface() { + // Avoid calling Error or String methods on nil receivers since many + // implementations crash when doing so. + if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() { + var prefix, strVal string + func() { + // Swallow and ignore any panics from String or Error. + defer func() { recover() }() + switch v := v.Interface().(type) { + case error: + strVal = v.Error() + prefix = "e" + case fmt.Stringer: + strVal = v.String() + prefix = "s" + } + }() + if prefix != "" { + return opts.formatString(prefix, strVal) + } + } + } + + // Check whether to explicitly wrap the result with the type. + var skipType bool + defer func() { + if !skipType { + out = opts.FormatType(t, out) + } + }() + + switch t.Kind() { + case reflect.Bool: + return textLine(fmt.Sprint(v.Bool())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return textLine(fmt.Sprint(v.Int())) + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return textLine(fmt.Sprint(v.Uint())) + case reflect.Uint8: + if parentKind == reflect.Slice || parentKind == reflect.Array { + return textLine(formatHex(v.Uint())) + } + return textLine(fmt.Sprint(v.Uint())) + case reflect.Uintptr: + return textLine(formatHex(v.Uint())) + case reflect.Float32, reflect.Float64: + return textLine(fmt.Sprint(v.Float())) + case reflect.Complex64, reflect.Complex128: + return textLine(fmt.Sprint(v.Complex())) + case reflect.String: + return opts.formatString("", v.String()) + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + return textLine(formatPointer(value.PointerOf(v), true)) + case reflect.Struct: + var list textList + v := makeAddressable(v) // needed for retrieveUnexportedField + maxLen := v.NumField() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + for i := 0; i < v.NumField(); i++ { + vv := v.Field(i) + if value.IsZero(vv) { + continue // Elide fields with zero values + } + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + sf := t.Field(i) + if supportExporters && !isExported(sf.Name) { + vv = retrieveUnexportedField(v, sf, true) + } + s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs) + list = append(list, textRecord{Key: sf.Name, Value: s}) + } + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} + case reflect.Slice: + if v.IsNil() { + return textNil + } + + // Check whether this is a []byte of text data. + if t.Elem() == reflect.TypeOf(byte(0)) { + b := v.Bytes() + isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) && unicode.IsSpace(r) } + if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { + out = opts.formatString("", string(b)) + return opts.WithTypeMode(emitType).FormatType(t, out) + } + } + + fallthrough + case reflect.Array: + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + var list textList + for i := 0; i < v.Len(); i++ { + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs) + list = append(list, textRecord{Value: s}) + } + + out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + if t.Kind() == reflect.Slice && opts.PrintAddresses { + header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap()) + out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out} + } + return out + case reflect.Map: + if v.IsNil() { + return textNil + } + + // Check pointer for cycles. + ptrRef, visited := ptrs.Push(v) + if visited { + return makeLeafReference(ptrRef, opts.PrintAddresses) + } + defer ptrs.Pop() + + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + var list textList + for _, k := range value.SortKeys(v.MapKeys()) { + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + sk := formatMapKey(k, false, ptrs) + sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs) + list = append(list, textRecord{Key: sk, Value: sv}) + } + + out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) + return out + case reflect.Ptr: + if v.IsNil() { + return textNil + } + + // Check pointer for cycles. + ptrRef, visited := ptrs.Push(v) + if visited { + out = makeLeafReference(ptrRef, opts.PrintAddresses) + return &textWrap{Prefix: "&", Value: out} + } + defer ptrs.Pop() + + skipType = true // Let the underlying value print the type instead + out = opts.FormatValue(v.Elem(), t.Kind(), ptrs) + out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) + out = &textWrap{Prefix: "&", Value: out} + return out + case reflect.Interface: + if v.IsNil() { + return textNil + } + // Interfaces accept different concrete types, + // so configure the underlying value to explicitly print the type. + skipType = true // Print the concrete type instead + return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs) + default: + panic(fmt.Sprintf("%v kind not handled", v.Kind())) + } +} + +func (opts formatOptions) formatString(prefix, s string) textNode { + maxLen := len(s) + maxLines := strings.Count(s, "\n") + 1 + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... + maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... + } + + // For multiline strings, use the triple-quote syntax, + // but only use it when printing removed or inserted nodes since + // we only want the extra verbosity for those cases. + lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n") + isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+') + for i := 0; i < len(lines) && isTripleQuoted; i++ { + lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support + isPrintable := func(r rune) bool { + return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable + } + line := lines[i] + isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen + } + if isTripleQuoted { + var list textList + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) + for i, line := range lines { + if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 { + comment := commentString(fmt.Sprintf("%d elided lines", numElided)) + list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment}) + break + } + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true}) + } + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) + return &textWrap{Prefix: "(", Value: list, Suffix: ")"} + } + + // Format the string as a single-line quoted string. + if len(s) > maxLen+len(textEllipsis) { + return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis)) + } + return textLine(prefix + formatString(s)) +} + +// formatMapKey formats v as if it were a map key. +// The result is guaranteed to be a single line. +func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string { + var opts formatOptions + opts.DiffMode = diffIdentical + opts.TypeMode = elideType + opts.PrintAddresses = disambiguate + opts.AvoidStringer = disambiguate + opts.QualifiedNames = disambiguate + opts.VerbosityLevel = maxVerbosityPreset + opts.LimitVerbosity = true + s := opts.FormatValue(v, reflect.Map, ptrs).String() + return strings.TrimSpace(s) +} + +// formatString prints s as a double-quoted or backtick-quoted string. +func formatString(s string) string { + // Use quoted string if it the same length as a raw string literal. + // Otherwise, attempt to use the raw string form. + qs := strconv.Quote(s) + if len(qs) == 1+len(s)+1 { + return qs + } + + // Disallow newlines to ensure output is a single line. + // Only allow printable runes for readability purposes. + rawInvalid := func(r rune) bool { + return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t') + } + if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 { + return "`" + s + "`" + } + return qs +} + +// formatHex prints u as a hexadecimal integer in Go notation. +func formatHex(u uint64) string { + var f string + switch { + case u <= 0xff: + f = "0x%02x" + case u <= 0xffff: + f = "0x%04x" + case u <= 0xffffff: + f = "0x%06x" + case u <= 0xffffffff: + f = "0x%08x" + case u <= 0xffffffffff: + f = "0x%010x" + case u <= 0xffffffffffff: + f = "0x%012x" + case u <= 0xffffffffffffff: + f = "0x%014x" + case u <= 0xffffffffffffffff: + f = "0x%016x" + } + return fmt.Sprintf(f, u) +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_slices.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_slices.go new file mode 100644 index 0000000..168f92f --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_slices.go @@ -0,0 +1,465 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/diff" +) + +// CanFormatDiffSlice reports whether we support custom formatting for nodes +// that are slices of primitive kinds or strings. +func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { + switch { + case opts.DiffMode != diffUnknown: + return false // Must be formatting in diff mode + case v.NumDiff == 0: + return false // No differences detected + case !v.ValueX.IsValid() || !v.ValueY.IsValid(): + return false // Both values must be valid + case v.NumIgnored > 0: + return false // Some ignore option was used + case v.NumTransformed > 0: + return false // Some transform option was used + case v.NumCompared > 1: + return false // More than one comparison was used + case v.NumCompared == 1 && v.Type.Name() != "": + // The need for cmp to check applicability of options on every element + // in a slice is a significant performance detriment for large []byte. + // The workaround is to specify Comparer(bytes.Equal), + // which enables cmp to compare []byte more efficiently. + // If they differ, we still want to provide batched diffing. + // The logic disallows named types since they tend to have their own + // String method, with nicer formatting than what this provides. + return false + } + + // Check whether this is an interface with the same concrete types. + t := v.Type + vx, vy := v.ValueX, v.ValueY + if t.Kind() == reflect.Interface && !vx.IsNil() && !vy.IsNil() && vx.Elem().Type() == vy.Elem().Type() { + vx, vy = vx.Elem(), vy.Elem() + t = vx.Type() + } + + // Check whether we provide specialized diffing for this type. + switch t.Kind() { + case reflect.String: + case reflect.Array, reflect.Slice: + // Only slices of primitive types have specialized handling. + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + default: + return false + } + + // Both slice values have to be non-empty. + if t.Kind() == reflect.Slice && (vx.Len() == 0 || vy.Len() == 0) { + return false + } + + // If a sufficient number of elements already differ, + // use specialized formatting even if length requirement is not met. + if v.NumDiff > v.NumSame { + return true + } + default: + return false + } + + // Use specialized string diffing for longer slices or strings. + const minLength = 64 + return vx.Len() >= minLength && vy.Len() >= minLength +} + +// FormatDiffSlice prints a diff for the slices (or strings) represented by v. +// This provides custom-tailored logic to make printing of differences in +// textual strings and slices of primitive kinds more readable. +func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { + assert(opts.DiffMode == diffUnknown) + t, vx, vy := v.Type, v.ValueX, v.ValueY + if t.Kind() == reflect.Interface { + vx, vy = vx.Elem(), vy.Elem() + t = vx.Type() + opts = opts.WithTypeMode(emitType) + } + + // Auto-detect the type of the data. + var isLinedText, isText, isBinary bool + var sx, sy string + switch { + case t.Kind() == reflect.String: + sx, sy = vx.String(), vy.String() + isText = true // Initial estimate, verify later + case t.Kind() == reflect.Slice && t.Elem() == reflect.TypeOf(byte(0)): + sx, sy = string(vx.Bytes()), string(vy.Bytes()) + isBinary = true // Initial estimate, verify later + case t.Kind() == reflect.Array: + // Arrays need to be addressable for slice operations to work. + vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem() + vx2.Set(vx) + vy2.Set(vy) + vx, vy = vx2, vy2 + } + if isText || isBinary { + var numLines, lastLineIdx, maxLineLen int + isBinary = !utf8.ValidString(sx) || !utf8.ValidString(sy) + for i, r := range sx + sy { + if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError { + isBinary = true + break + } + if r == '\n' { + if maxLineLen < i-lastLineIdx { + maxLineLen = i - lastLineIdx + } + lastLineIdx = i + 1 + numLines++ + } + } + isText = !isBinary + isLinedText = isText && numLines >= 4 && maxLineLen <= 1024 + } + + // Format the string into printable records. + var list textList + var delim string + switch { + // If the text appears to be multi-lined text, + // then perform differencing across individual lines. + case isLinedText: + ssx := strings.Split(sx, "\n") + ssy := strings.Split(sy, "\n") + list = opts.formatDiffSlice( + reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line", + func(v reflect.Value, d diffMode) textRecord { + s := formatString(v.Index(0).String()) + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + delim = "\n" + + // If possible, use a custom triple-quote (""") syntax for printing + // differences in a string literal. This format is more readable, + // but has edge-cases where differences are visually indistinguishable. + // This format is avoided under the following conditions: + // • A line starts with `"""` + // • A line starts with "..." + // • A line contains non-printable characters + // • Adjacent different lines differ only by whitespace + // + // For example: + // """ + // ... // 3 identical lines + // foo + // bar + // - baz + // + BAZ + // """ + isTripleQuoted := true + prevRemoveLines := map[string]bool{} + prevInsertLines := map[string]bool{} + var list2 textList + list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) + for _, r := range list { + if !r.Value.Equal(textEllipsis) { + line, _ := strconv.Unquote(string(r.Value.(textLine))) + line = strings.TrimPrefix(strings.TrimSuffix(line, "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support + normLine := strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 // drop whitespace to avoid visually indistinguishable output + } + return r + }, line) + isPrintable := func(r rune) bool { + return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable + } + isTripleQuoted = !strings.HasPrefix(line, `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" + switch r.Diff { + case diffRemoved: + isTripleQuoted = isTripleQuoted && !prevInsertLines[normLine] + prevRemoveLines[normLine] = true + case diffInserted: + isTripleQuoted = isTripleQuoted && !prevRemoveLines[normLine] + prevInsertLines[normLine] = true + } + if !isTripleQuoted { + break + } + r.Value = textLine(line) + r.ElideComma = true + } + if !(r.Diff == diffRemoved || r.Diff == diffInserted) { // start a new non-adjacent difference group + prevRemoveLines = map[string]bool{} + prevInsertLines = map[string]bool{} + } + list2 = append(list2, r) + } + if r := list2[len(list2)-1]; r.Diff == diffIdentical && len(r.Value.(textLine)) == 0 { + list2 = list2[:len(list2)-1] // elide single empty line at the end + } + list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) + if isTripleQuoted { + var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"} + switch t.Kind() { + case reflect.String: + if t != reflect.TypeOf(string("")) { + out = opts.FormatType(t, out) + } + case reflect.Slice: + // Always emit type for slices since the triple-quote syntax + // looks like a string (not a slice). + opts = opts.WithTypeMode(emitType) + out = opts.FormatType(t, out) + } + return out + } + + // If the text appears to be single-lined text, + // then perform differencing in approximately fixed-sized chunks. + // The output is printed as quoted strings. + case isText: + list = opts.formatDiffSlice( + reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte", + func(v reflect.Value, d diffMode) textRecord { + s := formatString(v.String()) + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + delim = "" + + // If the text appears to be binary data, + // then perform differencing in approximately fixed-sized chunks. + // The output is inspired by hexdump. + case isBinary: + list = opts.formatDiffSlice( + reflect.ValueOf(sx), reflect.ValueOf(sy), 16, "byte", + func(v reflect.Value, d diffMode) textRecord { + var ss []string + for i := 0; i < v.Len(); i++ { + ss = append(ss, formatHex(v.Index(i).Uint())) + } + s := strings.Join(ss, ", ") + comment := commentString(fmt.Sprintf("%c|%v|", d, formatASCII(v.String()))) + return textRecord{Diff: d, Value: textLine(s), Comment: comment} + }, + ) + + // For all other slices of primitive types, + // then perform differencing in approximately fixed-sized chunks. + // The size of each chunk depends on the width of the element kind. + default: + var chunkSize int + if t.Elem().Kind() == reflect.Bool { + chunkSize = 16 + } else { + switch t.Elem().Bits() { + case 8: + chunkSize = 16 + case 16: + chunkSize = 12 + case 32: + chunkSize = 8 + default: + chunkSize = 8 + } + } + list = opts.formatDiffSlice( + vx, vy, chunkSize, t.Elem().Kind().String(), + func(v reflect.Value, d diffMode) textRecord { + var ss []string + for i := 0; i < v.Len(); i++ { + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ss = append(ss, fmt.Sprint(v.Index(i).Int())) + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + ss = append(ss, fmt.Sprint(v.Index(i).Uint())) + case reflect.Uint8, reflect.Uintptr: + ss = append(ss, formatHex(v.Index(i).Uint())) + case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + ss = append(ss, fmt.Sprint(v.Index(i).Interface())) + } + } + s := strings.Join(ss, ", ") + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + } + + // Wrap the output with appropriate type information. + var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + if !isText { + // The "{...}" byte-sequence literal is not valid Go syntax for strings. + // Emit the type for extra clarity (e.g. "string{...}"). + if t.Kind() == reflect.String { + opts = opts.WithTypeMode(emitType) + } + return opts.FormatType(t, out) + } + switch t.Kind() { + case reflect.String: + out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} + if t != reflect.TypeOf(string("")) { + out = opts.FormatType(t, out) + } + case reflect.Slice: + out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} + if t != reflect.TypeOf([]byte(nil)) { + out = opts.FormatType(t, out) + } + } + return out +} + +// formatASCII formats s as an ASCII string. +// This is useful for printing binary strings in a semi-legible way. +func formatASCII(s string) string { + b := bytes.Repeat([]byte{'.'}, len(s)) + for i := 0; i < len(s); i++ { + if ' ' <= s[i] && s[i] <= '~' { + b[i] = s[i] + } + } + return string(b) +} + +func (opts formatOptions) formatDiffSlice( + vx, vy reflect.Value, chunkSize int, name string, + makeRec func(reflect.Value, diffMode) textRecord, +) (list textList) { + es := diff.Difference(vx.Len(), vy.Len(), func(ix int, iy int) diff.Result { + return diff.BoolResult(vx.Index(ix).Interface() == vy.Index(iy).Interface()) + }) + + appendChunks := func(v reflect.Value, d diffMode) int { + n0 := v.Len() + for v.Len() > 0 { + n := chunkSize + if n > v.Len() { + n = v.Len() + } + list = append(list, makeRec(v.Slice(0, n), d)) + v = v.Slice(n, v.Len()) + } + return n0 - v.Len() + } + + var numDiffs int + maxLen := -1 + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... + opts.VerbosityLevel-- + } + + groups := coalesceAdjacentEdits(name, es) + groups = coalesceInterveningIdentical(groups, chunkSize/4) + maxGroup := diffStats{Name: name} + for i, ds := range groups { + if maxLen >= 0 && numDiffs >= maxLen { + maxGroup = maxGroup.Append(ds) + continue + } + + // Print equal. + if ds.NumDiff() == 0 { + // Compute the number of leading and trailing equal bytes to print. + var numLo, numHi int + numEqual := ds.NumIgnored + ds.NumIdentical + for numLo < chunkSize*numContextRecords && numLo+numHi < numEqual && i != 0 { + numLo++ + } + for numHi < chunkSize*numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { + numHi++ + } + if numEqual-(numLo+numHi) <= chunkSize && ds.NumIgnored == 0 { + numHi = numEqual - numLo // Avoid pointless coalescing of single equal row + } + + // Print the equal bytes. + appendChunks(vx.Slice(0, numLo), diffIdentical) + if numEqual > numLo+numHi { + ds.NumIdentical -= numLo + numHi + list.AppendEllipsis(ds) + } + appendChunks(vx.Slice(numEqual-numHi, numEqual), diffIdentical) + vx = vx.Slice(numEqual, vx.Len()) + vy = vy.Slice(numEqual, vy.Len()) + continue + } + + // Print unequal. + len0 := len(list) + nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved) + vx = vx.Slice(nx, vx.Len()) + ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted) + vy = vy.Slice(ny, vy.Len()) + numDiffs += len(list) - len0 + } + if maxGroup.IsZero() { + assert(vx.Len() == 0 && vy.Len() == 0) + } else { + list.AppendEllipsis(maxGroup) + } + return list +} + +// coalesceAdjacentEdits coalesces the list of edits into groups of adjacent +// equal or unequal counts. +func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) { + var prevCase int // Arbitrary index into which case last occurred + lastStats := func(i int) *diffStats { + if prevCase != i { + groups = append(groups, diffStats{Name: name}) + prevCase = i + } + return &groups[len(groups)-1] + } + for _, e := range es { + switch e { + case diff.Identity: + lastStats(1).NumIdentical++ + case diff.UniqueX: + lastStats(2).NumRemoved++ + case diff.UniqueY: + lastStats(2).NumInserted++ + case diff.Modified: + lastStats(2).NumModified++ + } + } + return groups +} + +// coalesceInterveningIdentical coalesces sufficiently short (<= windowSize) +// equal groups into adjacent unequal groups that currently result in a +// dual inserted/removed printout. This acts as a high-pass filter to smooth +// out high-frequency changes within the windowSize. +func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats { + groups, groupsOrig := groups[:0], groups + for i, ds := range groupsOrig { + if len(groups) >= 2 && ds.NumDiff() > 0 { + prev := &groups[len(groups)-2] // Unequal group + curr := &groups[len(groups)-1] // Equal group + next := &groupsOrig[i] // Unequal group + hadX, hadY := prev.NumRemoved > 0, prev.NumInserted > 0 + hasX, hasY := next.NumRemoved > 0, next.NumInserted > 0 + if ((hadX || hasX) && (hadY || hasY)) && curr.NumIdentical <= windowSize { + *prev = prev.Append(*curr).Append(*next) + groups = groups[:len(groups)-1] // Truncate off equal group + continue + } + } + groups = append(groups, ds) + } + return groups +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_text.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_text.go new file mode 100644 index 0000000..0fd46d7 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_text.go @@ -0,0 +1,431 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "bytes" + "fmt" + "math/rand" + "strings" + "time" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/flags" +) + +var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 + +const maxColumnLength = 80 + +type indentMode int + +func (n indentMode) appendIndent(b []byte, d diffMode) []byte { + // The output of Diff is documented as being unstable to provide future + // flexibility in changing the output for more humanly readable reports. + // This logic intentionally introduces instability to the exact output + // so that users can detect accidental reliance on stability early on, + // rather than much later when an actual change to the format occurs. + if flags.Deterministic || randBool { + // Use regular spaces (U+0020). + switch d { + case diffUnknown, diffIdentical: + b = append(b, " "...) + case diffRemoved: + b = append(b, "- "...) + case diffInserted: + b = append(b, "+ "...) + } + } else { + // Use non-breaking spaces (U+00a0). + switch d { + case diffUnknown, diffIdentical: + b = append(b, "  "...) + case diffRemoved: + b = append(b, "- "...) + case diffInserted: + b = append(b, "+ "...) + } + } + return repeatCount(n).appendChar(b, '\t') +} + +type repeatCount int + +func (n repeatCount) appendChar(b []byte, c byte) []byte { + for ; n > 0; n-- { + b = append(b, c) + } + return b +} + +// textNode is a simplified tree-based representation of structured text. +// Possible node types are textWrap, textList, or textLine. +type textNode interface { + // Len reports the length in bytes of a single-line version of the tree. + // Nested textRecord.Diff and textRecord.Comment fields are ignored. + Len() int + // Equal reports whether the two trees are structurally identical. + // Nested textRecord.Diff and textRecord.Comment fields are compared. + Equal(textNode) bool + // String returns the string representation of the text tree. + // It is not guaranteed that len(x.String()) == x.Len(), + // nor that x.String() == y.String() implies that x.Equal(y). + String() string + + // formatCompactTo formats the contents of the tree as a single-line string + // to the provided buffer. Any nested textRecord.Diff and textRecord.Comment + // fields are ignored. + // + // However, not all nodes in the tree should be collapsed as a single-line. + // If a node can be collapsed as a single-line, it is replaced by a textLine + // node. Since the top-level node cannot replace itself, this also returns + // the current node itself. + // + // This does not mutate the receiver. + formatCompactTo([]byte, diffMode) ([]byte, textNode) + // formatExpandedTo formats the contents of the tree as a multi-line string + // to the provided buffer. In order for column alignment to operate well, + // formatCompactTo must be called before calling formatExpandedTo. + formatExpandedTo([]byte, diffMode, indentMode) []byte +} + +// textWrap is a wrapper that concatenates a prefix and/or a suffix +// to the underlying node. +type textWrap struct { + Prefix string // e.g., "bytes.Buffer{" + Value textNode // textWrap | textList | textLine + Suffix string // e.g., "}" + Metadata interface{} // arbitrary metadata; has no effect on formatting +} + +func (s *textWrap) Len() int { + return len(s.Prefix) + s.Value.Len() + len(s.Suffix) +} +func (s1 *textWrap) Equal(s2 textNode) bool { + if s2, ok := s2.(*textWrap); ok { + return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix + } + return false +} +func (s *textWrap) String() string { + var d diffMode + var n indentMode + _, s2 := s.formatCompactTo(nil, d) + b := n.appendIndent(nil, d) // Leading indent + b = s2.formatExpandedTo(b, d, n) // Main body + b = append(b, '\n') // Trailing newline + return string(b) +} +func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + n0 := len(b) // Original buffer length + b = append(b, s.Prefix...) + b, s.Value = s.Value.formatCompactTo(b, d) + b = append(b, s.Suffix...) + if _, ok := s.Value.(textLine); ok { + return b, textLine(b[n0:]) + } + return b, s +} +func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { + b = append(b, s.Prefix...) + b = s.Value.formatExpandedTo(b, d, n) + b = append(b, s.Suffix...) + return b +} + +// textList is a comma-separated list of textWrap or textLine nodes. +// The list may be formatted as multi-lines or single-line at the discretion +// of the textList.formatCompactTo method. +type textList []textRecord +type textRecord struct { + Diff diffMode // e.g., 0 or '-' or '+' + Key string // e.g., "MyField" + Value textNode // textWrap | textLine + ElideComma bool // avoid trailing comma + Comment fmt.Stringer // e.g., "6 identical fields" +} + +// AppendEllipsis appends a new ellipsis node to the list if none already +// exists at the end. If cs is non-zero it coalesces the statistics with the +// previous diffStats. +func (s *textList) AppendEllipsis(ds diffStats) { + hasStats := !ds.IsZero() + if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) { + if hasStats { + *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds}) + } else { + *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true}) + } + return + } + if hasStats { + (*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds) + } +} + +func (s textList) Len() (n int) { + for i, r := range s { + n += len(r.Key) + if r.Key != "" { + n += len(": ") + } + n += r.Value.Len() + if i < len(s)-1 { + n += len(", ") + } + } + return n +} + +func (s1 textList) Equal(s2 textNode) bool { + if s2, ok := s2.(textList); ok { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + r1, r2 := s1[i], s2[i] + if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) { + return false + } + } + return true + } + return false +} + +func (s textList) String() string { + return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String() +} + +func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + s = append(textList(nil), s...) // Avoid mutating original + + // Determine whether we can collapse this list as a single line. + n0 := len(b) // Original buffer length + var multiLine bool + for i, r := range s { + if r.Diff == diffInserted || r.Diff == diffRemoved { + multiLine = true + } + b = append(b, r.Key...) + if r.Key != "" { + b = append(b, ": "...) + } + b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff) + if _, ok := s[i].Value.(textLine); !ok { + multiLine = true + } + if r.Comment != nil { + multiLine = true + } + if i < len(s)-1 { + b = append(b, ", "...) + } + } + // Force multi-lined output when printing a removed/inserted node that + // is sufficiently long. + if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength { + multiLine = true + } + if !multiLine { + return b, textLine(b[n0:]) + } + return b, s +} + +func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { + alignKeyLens := s.alignLens( + func(r textRecord) bool { + _, isLine := r.Value.(textLine) + return r.Key == "" || !isLine + }, + func(r textRecord) int { return utf8.RuneCountInString(r.Key) }, + ) + alignValueLens := s.alignLens( + func(r textRecord) bool { + _, isLine := r.Value.(textLine) + return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil + }, + func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) }, + ) + + // Format lists of simple lists in a batched form. + // If the list is sequence of only textLine values, + // then batch multiple values on a single line. + var isSimple bool + for _, r := range s { + _, isLine := r.Value.(textLine) + isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil + if !isSimple { + break + } + } + if isSimple { + n++ + var batch []byte + emitBatch := func() { + if len(batch) > 0 { + b = n.appendIndent(append(b, '\n'), d) + b = append(b, bytes.TrimRight(batch, " ")...) + batch = batch[:0] + } + } + for _, r := range s { + line := r.Value.(textLine) + if len(batch)+len(line)+len(", ") > maxColumnLength { + emitBatch() + } + batch = append(batch, line...) + batch = append(batch, ", "...) + } + emitBatch() + n-- + return n.appendIndent(append(b, '\n'), d) + } + + // Format the list as a multi-lined output. + n++ + for i, r := range s { + b = n.appendIndent(append(b, '\n'), d|r.Diff) + if r.Key != "" { + b = append(b, r.Key+": "...) + } + b = alignKeyLens[i].appendChar(b, ' ') + + b = r.Value.formatExpandedTo(b, d|r.Diff, n) + if !r.ElideComma { + b = append(b, ',') + } + b = alignValueLens[i].appendChar(b, ' ') + + if r.Comment != nil { + b = append(b, " // "+r.Comment.String()...) + } + } + n-- + + return n.appendIndent(append(b, '\n'), d) +} + +func (s textList) alignLens( + skipFunc func(textRecord) bool, + lenFunc func(textRecord) int, +) []repeatCount { + var startIdx, endIdx, maxLen int + lens := make([]repeatCount, len(s)) + for i, r := range s { + if skipFunc(r) { + for j := startIdx; j < endIdx && j < len(s); j++ { + lens[j] = repeatCount(maxLen - lenFunc(s[j])) + } + startIdx, endIdx, maxLen = i+1, i+1, 0 + } else { + if maxLen < lenFunc(r) { + maxLen = lenFunc(r) + } + endIdx = i + 1 + } + } + for j := startIdx; j < endIdx && j < len(s); j++ { + lens[j] = repeatCount(maxLen - lenFunc(s[j])) + } + return lens +} + +// textLine is a single-line segment of text and is always a leaf node +// in the textNode tree. +type textLine []byte + +var ( + textNil = textLine("nil") + textEllipsis = textLine("...") +) + +func (s textLine) Len() int { + return len(s) +} +func (s1 textLine) Equal(s2 textNode) bool { + if s2, ok := s2.(textLine); ok { + return bytes.Equal([]byte(s1), []byte(s2)) + } + return false +} +func (s textLine) String() string { + return string(s) +} +func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + return append(b, s...), s +} +func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte { + return append(b, s...) +} + +type diffStats struct { + Name string + NumIgnored int + NumIdentical int + NumRemoved int + NumInserted int + NumModified int +} + +func (s diffStats) IsZero() bool { + s.Name = "" + return s == diffStats{} +} + +func (s diffStats) NumDiff() int { + return s.NumRemoved + s.NumInserted + s.NumModified +} + +func (s diffStats) Append(ds diffStats) diffStats { + assert(s.Name == ds.Name) + s.NumIgnored += ds.NumIgnored + s.NumIdentical += ds.NumIdentical + s.NumRemoved += ds.NumRemoved + s.NumInserted += ds.NumInserted + s.NumModified += ds.NumModified + return s +} + +// String prints a humanly-readable summary of coalesced records. +// +// Example: +// diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields" +func (s diffStats) String() string { + var ss []string + var sum int + labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"} + counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified} + for i, n := range counts { + if n > 0 { + ss = append(ss, fmt.Sprintf("%d %v", n, labels[i])) + } + sum += n + } + + // Pluralize the name (adjusting for some obscure English grammar rules). + name := s.Name + if sum > 1 { + name += "s" + if strings.HasSuffix(name, "ys") { + name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries" + } + } + + // Format the list according to English grammar (with Oxford comma). + switch n := len(ss); n { + case 0: + return "" + case 1, 2: + return strings.Join(ss, " and ") + " " + name + default: + return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name + } +} + +type commentString string + +func (s commentString) String() string { return string(s) } diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_value.go b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_value.go new file mode 100644 index 0000000..668d470 --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/report_value.go @@ -0,0 +1,121 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import "reflect" + +// valueNode represents a single node within a report, which is a +// structured representation of the value tree, containing information +// regarding which nodes are equal or not. +type valueNode struct { + parent *valueNode + + Type reflect.Type + ValueX reflect.Value + ValueY reflect.Value + + // NumSame is the number of leaf nodes that are equal. + // All descendants are equal only if NumDiff is 0. + NumSame int + // NumDiff is the number of leaf nodes that are not equal. + NumDiff int + // NumIgnored is the number of leaf nodes that are ignored. + NumIgnored int + // NumCompared is the number of leaf nodes that were compared + // using an Equal method or Comparer function. + NumCompared int + // NumTransformed is the number of non-leaf nodes that were transformed. + NumTransformed int + // NumChildren is the number of transitive descendants of this node. + // This counts from zero; thus, leaf nodes have no descendants. + NumChildren int + // MaxDepth is the maximum depth of the tree. This counts from zero; + // thus, leaf nodes have a depth of zero. + MaxDepth int + + // Records is a list of struct fields, slice elements, or map entries. + Records []reportRecord // If populated, implies Value is not populated + + // Value is the result of a transformation, pointer indirect, of + // type assertion. + Value *valueNode // If populated, implies Records is not populated + + // TransformerName is the name of the transformer. + TransformerName string // If non-empty, implies Value is populated +} +type reportRecord struct { + Key reflect.Value // Invalid for slice element + Value *valueNode +} + +func (parent *valueNode) PushStep(ps PathStep) (child *valueNode) { + vx, vy := ps.Values() + child = &valueNode{parent: parent, Type: ps.Type(), ValueX: vx, ValueY: vy} + switch s := ps.(type) { + case StructField: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Key: reflect.ValueOf(s.Name()), Value: child}) + case SliceIndex: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Value: child}) + case MapIndex: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Key: s.Key(), Value: child}) + case Indirect: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + case TypeAssertion: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + case Transform: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + parent.TransformerName = s.Name() + parent.NumTransformed++ + default: + assert(parent == nil) // Must be the root step + } + return child +} + +func (r *valueNode) Report(rs Result) { + assert(r.MaxDepth == 0) // May only be called on leaf nodes + + if rs.ByIgnore() { + r.NumIgnored++ + } else { + if rs.Equal() { + r.NumSame++ + } else { + r.NumDiff++ + } + } + assert(r.NumSame+r.NumDiff+r.NumIgnored == 1) + + if rs.ByMethod() { + r.NumCompared++ + } + if rs.ByFunc() { + r.NumCompared++ + } + assert(r.NumCompared <= 1) +} + +func (child *valueNode) PopStep() (parent *valueNode) { + if child.parent == nil { + return nil + } + parent = child.parent + parent.NumSame += child.NumSame + parent.NumDiff += child.NumDiff + parent.NumIgnored += child.NumIgnored + parent.NumCompared += child.NumCompared + parent.NumTransformed += child.NumTransformed + parent.NumChildren += child.NumChildren + 1 + if parent.MaxDepth < child.MaxDepth+1 { + parent.MaxDepth = child.MaxDepth + 1 + } + return parent +} diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/testdata/diffs b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/testdata/diffs new file mode 100644 index 0000000..05118be --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/cmp/testdata/diffs @@ -0,0 +1,1706 @@ +<<< TestDiff/Comparer/StructInequal + struct{ A int; B int; C int }{ + A: 1, + B: 2, +- C: 3, ++ C: 4, + } +>>> TestDiff/Comparer/StructInequal +<<< TestDiff/Comparer/PointerStructInequal + &struct{ A *int }{ +- A: &4, ++ A: &5, + } +>>> TestDiff/Comparer/PointerStructInequal +<<< TestDiff/Comparer/StructNestedPointerInequal + &struct{ R *bytes.Buffer }{ +- R: s"", ++ R: nil, + } +>>> TestDiff/Comparer/StructNestedPointerInequal +<<< TestDiff/Comparer/RegexpInequal + []*regexp.Regexp{ + nil, +- s"a*b*c*", ++ s"a*b*d*", + } +>>> TestDiff/Comparer/RegexpInequal +<<< TestDiff/Comparer/TriplePointerInequal + &&&int( +- 0, ++ 1, + ) +>>> TestDiff/Comparer/TriplePointerInequal +<<< TestDiff/Comparer/StringerInequal + struct{ fmt.Stringer }( +- s"hello", ++ s"hello2", + ) +>>> TestDiff/Comparer/StringerInequal +<<< TestDiff/Comparer/DifferingHash + [16]uint8{ +- 0x0c, 0xc1, 0x75, 0xb9, 0xc0, 0xf1, 0xb6, 0xa8, 0x31, 0xc3, 0x99, 0xe2, 0x69, 0x77, 0x26, 0x61, ++ 0x92, 0xeb, 0x5f, 0xfe, 0xe6, 0xae, 0x2f, 0xec, 0x3a, 0xd7, 0x1c, 0x77, 0x75, 0x31, 0x57, 0x8f, + } +>>> TestDiff/Comparer/DifferingHash +<<< TestDiff/Comparer/NilStringer + interface{}( +- &fmt.Stringer(nil), + ) +>>> TestDiff/Comparer/NilStringer +<<< TestDiff/Comparer/TarHeaders + []cmp_test.tarHeader{ + { + ... // 4 identical fields + Size: 1, + ModTime: s"2009-11-10 23:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 2, + ModTime: s"2009-11-11 00:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 4, + ModTime: s"2009-11-11 01:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 8, + ModTime: s"2009-11-11 02:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 16, + ModTime: s"2009-11-11 03:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + } +>>> TestDiff/Comparer/TarHeaders +<<< TestDiff/Comparer/IrreflexiveComparison + []int{ +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), + } +>>> TestDiff/Comparer/IrreflexiveComparison +<<< TestDiff/Comparer/StringerMapKey + map[*testprotos.Stringer]*testprotos.Stringer( +- {s"hello": s"world"}, ++ nil, + ) +>>> TestDiff/Comparer/StringerMapKey +<<< TestDiff/Comparer/StringerBacktick + interface{}( +- []*testprotos.Stringer{s`multi\nline\nline\nline`}, + ) +>>> TestDiff/Comparer/StringerBacktick +<<< TestDiff/Comparer/DynamicMap + []interface{}{ + map[string]interface{}{ + "avg": float64(0.278), +- "hr": int(65), ++ "hr": float64(65), + "name": string("Mark McGwire"), + }, + map[string]interface{}{ + "avg": float64(0.288), +- "hr": int(63), ++ "hr": float64(63), + "name": string("Sammy Sosa"), + }, + } +>>> TestDiff/Comparer/DynamicMap +<<< TestDiff/Comparer/MapKeyPointer + map[*int]string{ +- &⟪0xdeadf00f⟫0: "hello", ++ &⟪0xdeadf00f⟫0: "world", + } +>>> TestDiff/Comparer/MapKeyPointer +<<< TestDiff/Comparer/IgnoreSliceElements + [2][]int{ + {..., 1, 2, 3, ...}, + { + ... // 6 ignored and 1 identical elements +- 20, ++ 2, + ... // 3 ignored elements + }, + } +>>> TestDiff/Comparer/IgnoreSliceElements +<<< TestDiff/Comparer/IgnoreMapEntries + [2]map[string]int{ + {"KEEP3": 3, "keep1": 1, "keep2": 2, ...}, + { + ... // 2 ignored entries + "keep1": 1, ++ "keep2": 2, + }, + } +>>> TestDiff/Comparer/IgnoreMapEntries +<<< TestDiff/Transformer/Uints + uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64( +- 0, ++ 1, + ))))))) +>>> TestDiff/Transformer/Uints +<<< TestDiff/Transformer/Filtered + []int{ + Inverse(λ, int64(0)), +- Inverse(λ, int64(-5)), ++ Inverse(λ, int64(3)), + Inverse(λ, int64(0)), +- Inverse(λ, int64(-1)), ++ Inverse(λ, int64(-5)), + } +>>> TestDiff/Transformer/Filtered +<<< TestDiff/Transformer/DisjointOutput + int(Inverse(λ, interface{}( +- string("zero"), ++ float64(1), + ))) +>>> TestDiff/Transformer/DisjointOutput +<<< TestDiff/Transformer/JSON + string(Inverse(ParseJSON, map[string]interface{}{ + "address": map[string]interface{}{ +- "city": string("Los Angeles"), ++ "city": string("New York"), + "postalCode": string("10021-3100"), +- "state": string("CA"), ++ "state": string("NY"), + "streetAddress": string("21 2nd Street"), + }, + "age": float64(25), + "children": []interface{}{}, + "firstName": string("John"), + "isAlive": bool(true), + "lastName": string("Smith"), + "phoneNumbers": []interface{}{ + map[string]interface{}{ +- "number": string("212 555-4321"), ++ "number": string("212 555-1234"), + "type": string("home"), + }, + map[string]interface{}{"number": string("646 555-4567"), "type": string("office")}, + map[string]interface{}{"number": string("123 456-7890"), "type": string("mobile")}, + }, ++ "spouse": nil, + })) +>>> TestDiff/Transformer/JSON +<<< TestDiff/Transformer/AcyclicString + cmp_test.StringBytes{ + String: Inverse(SplitString, []string{ + "some", + "multi", +- "Line", ++ "line", + "string", + }), + Bytes: []uint8(Inverse(SplitBytes, [][]uint8{ + {0x73, 0x6f, 0x6d, 0x65}, + {0x6d, 0x75, 0x6c, 0x74, ...}, + {0x6c, 0x69, 0x6e, 0x65}, + { +- 0x62, ++ 0x42, + 0x79, + 0x74, + ... // 2 identical elements + }, + })), + } +>>> TestDiff/Transformer/AcyclicString +<<< TestDiff/Reporter/PanicStringer + struct{ X fmt.Stringer }{ +- X: struct{ fmt.Stringer }{}, ++ X: s"", + } +>>> TestDiff/Reporter/PanicStringer +<<< TestDiff/Reporter/PanicError + struct{ X error }{ +- X: struct{ error }{}, ++ X: e"", + } +>>> TestDiff/Reporter/PanicError +<<< TestDiff/Reporter/AmbiguousType + interface{}( +- "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{}, ++ "github.com/google/go-cmp/cmp/internal/teststructs/foo2".Bar{}, + ) +>>> TestDiff/Reporter/AmbiguousType +<<< TestDiff/Reporter/AmbiguousPointer + (*int)( +- &⟪0xdeadf00f⟫0, ++ &⟪0xdeadf00f⟫0, + ) +>>> TestDiff/Reporter/AmbiguousPointer +<<< TestDiff/Reporter/AmbiguousPointerStruct + struct{ I *int }{ +- I: &⟪0xdeadf00f⟫0, ++ I: &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerStruct +<<< TestDiff/Reporter/AmbiguousPointerSlice + []*int{ +- &⟪0xdeadf00f⟫0, ++ &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerSlice +<<< TestDiff/Reporter/AmbiguousPointerMap + map[string]*int{ +- "zero": &⟪0xdeadf00f⟫0, ++ "zero": &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerMap +<<< TestDiff/Reporter/AmbiguousStringer + interface{}( +- cmp_test.Stringer("hello"), ++ &cmp_test.Stringer("hello"), + ) +>>> TestDiff/Reporter/AmbiguousStringer +<<< TestDiff/Reporter/AmbiguousStringerStruct + struct{ S fmt.Stringer }{ +- S: cmp_test.Stringer("hello"), ++ S: &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerStruct +<<< TestDiff/Reporter/AmbiguousStringerSlice + []fmt.Stringer{ +- cmp_test.Stringer("hello"), ++ &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerSlice +<<< TestDiff/Reporter/AmbiguousStringerMap + map[string]fmt.Stringer{ +- "zero": cmp_test.Stringer("hello"), ++ "zero": &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerMap +<<< TestDiff/Reporter/AmbiguousSliceHeader + []int( +- ⟪ptr:0xdeadf00f, len:0, cap:5⟫{}, ++ ⟪ptr:0xdeadf00f, len:0, cap:1000⟫{}, + ) +>>> TestDiff/Reporter/AmbiguousSliceHeader +<<< TestDiff/Reporter/AmbiguousStringerMapKey + map[interface{}]string{ +- nil: "nil", ++ &⟪0xdeadf00f⟫"github.com/google/go-cmp/cmp_test".Stringer("hello"): "goodbye", +- "github.com/google/go-cmp/cmp_test".Stringer("hello"): "goodbye", +- "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{S: "fizz"}: "buzz", ++ "github.com/google/go-cmp/cmp/internal/teststructs/foo2".Bar{S: "fizz"}: "buzz", + } +>>> TestDiff/Reporter/AmbiguousStringerMapKey +<<< TestDiff/Reporter/NonAmbiguousStringerMapKey + map[interface{}]string{ ++ s"fizz": "buzz", +- s"hello": "goodbye", + } +>>> TestDiff/Reporter/NonAmbiguousStringerMapKey +<<< TestDiff/Reporter/InvalidUTF8 + interface{}( +- cmp_test.MyString("\xed\xa0\x80"), + ) +>>> TestDiff/Reporter/InvalidUTF8 +<<< TestDiff/Reporter/UnbatchedSlice + cmp_test.MyComposite{ + ... // 3 identical fields + BytesB: nil, + BytesC: nil, + IntsA: []int8{ ++ 10, + 11, +- 12, ++ 21, + 13, + 14, + ... // 15 identical elements + }, + IntsB: nil, + IntsC: nil, + ... // 6 identical fields + } +>>> TestDiff/Reporter/UnbatchedSlice +<<< TestDiff/Reporter/BatchedSlice + cmp_test.MyComposite{ + ... // 3 identical fields + BytesB: nil, + BytesC: nil, + IntsA: []int8{ +- 10, 11, 12, 13, 14, 15, 16, ++ 12, 29, 13, 27, 22, 23, + 17, 18, 19, 20, 21, +- 22, 23, 24, 25, 26, 27, 28, 29, ++ 10, 26, 16, 25, 28, 11, 15, 24, 14, + }, + IntsB: nil, + IntsC: nil, + ... // 6 identical fields + } +>>> TestDiff/Reporter/BatchedSlice +<<< TestDiff/Reporter/BatchedWithComparer + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: []uint8{ +- 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, // -|.......| ++ 0x0c, 0x1d, 0x0d, 0x1b, 0x16, 0x17, // +|......| + 0x11, 0x12, 0x13, 0x14, 0x15, // |.....| +- 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // -|........| ++ 0x0a, 0x1a, 0x10, 0x19, 0x1c, 0x0b, 0x0f, 0x18, 0x0e, // +|.........| + }, + BytesB: nil, + BytesC: nil, + ... // 9 identical fields + } +>>> TestDiff/Reporter/BatchedWithComparer +<<< TestDiff/Reporter/BatchedLong + interface{}( +- cmp_test.MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, ...}}, + ) +>>> TestDiff/Reporter/BatchedLong +<<< TestDiff/Reporter/BatchedNamedAndUnnamed + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: []uint8{ +- 0x01, 0x02, 0x03, // -|...| ++ 0x03, 0x02, 0x01, // +|...| + }, + BytesB: []cmp_test.MyByte{ +- 0x04, 0x05, 0x06, ++ 0x06, 0x05, 0x04, + }, + BytesC: cmp_test.MyBytes{ +- 0x07, 0x08, 0x09, // -|...| ++ 0x09, 0x08, 0x07, // +|...| + }, + IntsA: []int8{ +- -1, -2, -3, ++ -3, -2, -1, + }, + IntsB: []cmp_test.MyInt{ +- -4, -5, -6, ++ -6, -5, -4, + }, + IntsC: cmp_test.MyInts{ +- -7, -8, -9, ++ -9, -8, -7, + }, + UintsA: []uint16{ +- 1000, 2000, 3000, ++ 3000, 2000, 1000, + }, + UintsB: []cmp_test.MyUint{ +- 4000, 5000, 6000, ++ 6000, 5000, 4000, + }, + UintsC: cmp_test.MyUints{ +- 7000, 8000, 9000, ++ 9000, 8000, 7000, + }, + FloatsA: []float32{ +- 1.5, 2.5, 3.5, ++ 3.5, 2.5, 1.5, + }, + FloatsB: []cmp_test.MyFloat{ +- 4.5, 5.5, 6.5, ++ 6.5, 5.5, 4.5, + }, + FloatsC: cmp_test.MyFloats{ +- 7.5, 8.5, 9.5, ++ 9.5, 8.5, 7.5, + }, + } +>>> TestDiff/Reporter/BatchedNamedAndUnnamed +<<< TestDiff/Reporter/BinaryHexdump + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: []uint8{ + 0xf3, 0x0f, 0x8a, 0xa4, 0xd3, 0x12, 0x52, 0x09, 0x24, 0xbe, // |......R.$.| +- 0x58, 0x95, 0x41, 0xfd, 0x24, 0x66, 0x58, 0x8b, 0x79, // -|X.A.$fX.y| + 0x54, 0xac, 0x0d, 0xd8, 0x71, 0x77, 0x70, 0x20, 0x6a, 0x5c, 0x73, 0x7f, 0x8c, 0x17, 0x55, 0xc0, // |T...qwp j\s...U.| + 0x34, 0xce, 0x6e, 0xf7, 0xaa, 0x47, 0xee, 0x32, 0x9d, 0xc5, 0xca, 0x1e, 0x58, 0xaf, 0x8f, 0x27, // |4.n..G.2....X..'| + 0xf3, 0x02, 0x4a, 0x90, 0xed, 0x69, 0x2e, 0x70, 0x32, 0xb4, 0xab, 0x30, 0x20, 0xb6, 0xbd, 0x5c, // |..J..i.p2..0 ..\| + 0x62, 0x34, 0x17, 0xb0, 0x00, 0xbb, 0x4f, 0x7e, 0x27, 0x47, 0x06, 0xf4, 0x2e, 0x66, 0xfd, 0x63, // |b4....O~'G...f.c| + 0xd7, 0x04, 0xdd, 0xb7, 0x30, 0xb7, 0xd1, // |....0..| +- 0x55, 0x7e, 0x7b, 0xf6, 0xb3, 0x7e, 0x1d, 0x57, 0x69, // -|U~{..~.Wi| ++ 0x75, 0x2d, 0x5b, 0x5d, 0x5d, 0xf6, 0xb3, 0x68, 0x61, 0x68, 0x61, 0x7e, 0x1d, 0x57, 0x49, // +|u-[]]..haha~.WI| + 0x20, 0x9e, 0xbc, 0xdf, 0xe1, 0x4d, 0xa9, 0xef, 0xa2, 0xd2, 0xed, 0xb4, 0x47, 0x78, 0xc9, 0xc9, // | ....M......Gx..| + 0x27, 0xa4, 0xc6, 0xce, 0xec, 0x44, 0x70, 0x5d, // |'....Dp]| + }, + BytesB: nil, + BytesC: nil, + ... // 9 identical fields + } +>>> TestDiff/Reporter/BinaryHexdump +<<< TestDiff/Reporter/StringHexdump + cmp_test.MyComposite{ + StringA: "", + StringB: cmp_test.MyString{ +- 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, // -|readme| ++ 0x67, 0x6f, 0x70, 0x68, 0x65, 0x72, // +|gopher| + 0x2e, 0x74, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // |.txt............| + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // |................| + ... // 64 identical bytes + 0x30, 0x30, 0x36, 0x30, 0x30, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x30, // |00600.0000000.00| + 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x34, // |00000.0000000004| +- 0x36, // -|6| ++ 0x33, // +|3| + 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x31, 0x31, // |.00000000000.011| +- 0x31, 0x37, 0x33, // -|173| ++ 0x32, 0x31, 0x37, // +|217| + 0x00, 0x20, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // |. 0.............| + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // |................| + ... // 326 identical bytes + }, + BytesA: nil, + BytesB: nil, + ... // 10 identical fields + } +>>> TestDiff/Reporter/StringHexdump +<<< TestDiff/Reporter/BinaryString + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: bytes.Join({ + `{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"`, + `address":{"streetAddress":"`, +- "314 54th Avenue", ++ "21 2nd Street", + `","city":"New York","state":"NY","postalCode":"10021-3100"},"pho`, + `neNumbers":[{"type":"home","number":"212 555-1234"},{"type":"off`, + ... // 101 identical bytes + }, ""), + BytesB: nil, + BytesC: nil, + ... // 9 identical fields + } +>>> TestDiff/Reporter/BinaryString +<<< TestDiff/Reporter/TripleQuote + cmp_test.MyComposite{ + StringA: ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 6 identical lines + """ + ), + StringB: "", + BytesA: nil, + ... // 11 identical fields + } +>>> TestDiff/Reporter/TripleQuote +<<< TestDiff/Reporter/TripleQuoteSlice + []string{ + ( + """ + ... // 23 identical lines + xxx + yyy +- zzz + """ + ), + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\n"..., + } +>>> TestDiff/Reporter/TripleQuoteSlice +<<< TestDiff/Reporter/TripleQuoteNamedTypes + cmp_test.MyComposite{ + StringA: "", + StringB: ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 5 identical lines + """ + ), + BytesA: nil, + BytesB: nil, + BytesC: cmp_test.MyBytes( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 5 identical lines + """ + ), + IntsA: nil, + IntsB: nil, + ... // 7 identical fields + } +>>> TestDiff/Reporter/TripleQuoteNamedTypes +<<< TestDiff/Reporter/TripleQuoteSliceNamedTypes + []cmp_test.MyString{ + ( + """ + ... // 23 identical lines + xxx + yyy +- zzz + """ + ), + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\n"..., + } +>>> TestDiff/Reporter/TripleQuoteSliceNamedTypes +<<< TestDiff/Reporter/TripleQuoteEndlines + ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR ++ rrr + sss + ttt + ... // 4 identical lines + yyy + zzz +- + """ + ) +>>> TestDiff/Reporter/TripleQuoteEndlines +<<< TestDiff/Reporter/AvoidTripleQuoteAmbiguousQuotes + strings.Join({ + "aaa", + "bbb", +- "ccc", ++ "CCC", + "ddd", + "eee", +- "fff", ++ `"""`, + "ggg", + "hhh", + ... // 7 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteAmbiguousQuotes +<<< TestDiff/Reporter/AvoidTripleQuoteAmbiguousEllipsis + strings.Join({ + "aaa", + "bbb", +- "ccc", +- "...", ++ "CCC", ++ "ddd", + "eee", + "fff", + ... // 9 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteAmbiguousEllipsis +<<< TestDiff/Reporter/AvoidTripleQuoteNonPrintable + strings.Join({ + "aaa", + "bbb", +- "ccc", ++ "CCC", + "ddd", + "eee", + ... // 7 identical lines + "mmm", + "nnn", +- "ooo", ++ "o\roo", + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteNonPrintable +<<< TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace + strings.Join({ + "aaa", + "bbb", +- "ccc", +- " ddd", ++ "ccc ", ++ "ddd", + "eee", + "fff", + ... // 9 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace +<<< TestDiff/Reporter/TripleQuoteStringer + []fmt.Stringer{ + s"package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hel"..., +- ( +- s""" +- package main +- +- import ( +- "fmt" +- "math/rand" +- ) +- +- func main() { +- fmt.Println("My favorite number is", rand.Intn(10)) +- } +- s""" +- ), ++ ( ++ s""" ++ package main ++ ++ import ( ++ "fmt" ++ "math" ++ ) ++ ++ func main() { ++ fmt.Printf("Now you have %g problems.\n", math.Sqrt(7)) ++ } ++ s""" ++ ), + } +>>> TestDiff/Reporter/TripleQuoteStringer +<<< TestDiff/Reporter/LimitMaximumBytesDiffs + []uint8{ +- 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S| ++ 0x5c, 0x3d, 0x3d, 0x3d, 0x3d, 0x7c, 0x96, 0xe7, 0x53, 0x42, 0xa0, 0xab, // +|\====|..SB..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x1d, 0xdf, 0x61, 0xae, 0x98, 0x9f, 0x48, // -|..a...H| ++ 0xf0, 0xbd, 0xa5, 0x71, 0xab, 0x17, 0x3b, // +|...q..;| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0xc7, 0xb0, 0xb7, // -|...| ++ 0xab, 0x50, 0x00, // +|.P.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0xef, 0x3d, 0x3d, 0x3d, 0x3d, 0x3a, 0x5c, 0x94, 0xe6, 0x4a, 0xc7, // -|.====:\..J.| ++ 0xeb, 0x3d, 0x3d, 0x3d, 0x3d, 0xa5, 0x14, 0xe6, 0x4f, 0x28, 0xe4, // +|.====...O(.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0xb4, // -|.| ++ 0x28, // +|(| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x0a, 0x0a, 0xf7, 0x94, // -|....| ++ 0x2f, 0x63, 0x40, 0x3f, // +|/c@?| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| +- 0xf2, 0x9c, 0xc0, 0x66, // -|...f| ++ 0xd9, 0x78, 0xed, 0x13, // +|.x..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x34, 0xf6, 0xf1, 0xc3, 0x17, 0x82, // -|4.....| ++ 0x4a, 0xfc, 0x91, 0x38, 0x42, 0x8d, // +|J..8B.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x6e, 0x16, 0x60, 0x91, 0x44, 0xc6, 0x06, // -|n.`.D..| ++ 0x61, 0x38, 0x41, 0xeb, 0x73, 0x04, 0xae, // +|a8A.s..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0x1c, 0x45, 0x3d, 0x3d, 0x3d, 0x3d, 0x2e, // -|.E====.| ++ 0x07, 0x43, 0x3d, 0x3d, 0x3d, 0x3d, 0x1c, // +|.C====.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| +- 0xc4, 0x18, // -|..| ++ 0x91, 0x22, // +|."| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0x8a, 0x8d, 0x0e, 0x3d, 0x3d, 0x3d, 0x3d, 0x87, 0xb1, 0xa5, 0x8e, 0xc3, 0x3d, 0x3d, 0x3d, 0x3d, // -|...====.....====| +- 0x3d, 0x7a, 0x0f, 0x31, 0xae, 0x55, 0x3d, // -|=z.1.U=| ++ 0x75, 0xd8, 0xbe, 0x3d, 0x3d, 0x3d, 0x3d, 0x73, 0xec, 0x84, 0x35, 0x07, 0x3d, 0x3d, 0x3d, 0x3d, // +|u..====s..5.====| ++ 0x3d, 0x3b, 0xab, 0x53, 0x39, 0x74, // +|=;.S9t| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x47, 0x2c, 0x3d, // -|G,=| ++ 0x3d, 0x1f, 0x1b, // +|=..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x35, 0xe7, 0x35, 0xee, 0x82, 0xf4, 0xce, 0x3d, 0x3d, 0x3d, 0x3d, 0x11, 0x72, 0x3d, // -|5.5....====.r=| ++ 0x3d, 0x80, 0xab, 0x2f, 0xed, 0x2b, 0x3a, 0x3b, 0x3d, 0x3d, 0x3d, 0x3d, 0xea, 0x49, // +|=../.+:;====.I| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |==========| +- 0xaf, 0x5d, 0x3d, // -|.]=| ++ 0x3d, 0xab, 0x6c, // +|=.l| + ... // 51 identical, 34 removed, and 35 inserted bytes + } +>>> TestDiff/Reporter/LimitMaximumBytesDiffs +<<< TestDiff/Reporter/LimitMaximumStringDiffs + ( + """ +- a ++ aa + b +- c ++ cc + d +- e ++ ee + f +- g ++ gg + h +- i ++ ii + j +- k ++ kk + l +- m ++ mm + n +- o ++ oo + p +- q ++ qq + r +- s ++ ss + t +- u ++ uu + v +- w ++ ww + x +- y ++ yy + z +- A ++ AA + B +- C ++ CC + D +- E ++ EE + ... // 12 identical, 10 removed, and 10 inserted lines + """ + ) +>>> TestDiff/Reporter/LimitMaximumStringDiffs +<<< TestDiff/Reporter/LimitMaximumSliceDiffs + []struct{ S string }{ +- {S: "a"}, ++ {S: "aa"}, + {S: "b"}, +- {S: "c"}, ++ {S: "cc"}, + {S: "d"}, +- {S: "e"}, ++ {S: "ee"}, + {S: "f"}, +- {S: "g"}, ++ {S: "gg"}, + {S: "h"}, +- {S: "i"}, ++ {S: "ii"}, + {S: "j"}, +- {S: "k"}, ++ {S: "kk"}, + {S: "l"}, +- {S: "m"}, ++ {S: "mm"}, + {S: "n"}, +- {S: "o"}, ++ {S: "oo"}, + {S: "p"}, +- {S: "q"}, ++ {S: "qq"}, + {S: "r"}, +- {S: "s"}, ++ {S: "ss"}, + {S: "t"}, +- {S: "u"}, ++ {S: "uu"}, + {S: "v"}, +- {S: "w"}, ++ {S: "ww"}, + {S: "x"}, +- {S: "y"}, ++ {S: "yy"}, + {S: "z"}, +- {S: "A"}, ++ {S: "AA"}, + {S: "B"}, +- {S: "C"}, ++ {S: "CC"}, + {S: "D"}, +- {S: "E"}, ++ {S: "EE"}, + ... // 12 identical and 10 modified elements + } +>>> TestDiff/Reporter/LimitMaximumSliceDiffs +<<< TestDiff/Reporter/MultilineString + cmp_test.MyComposite{ + StringA: ( + """ +- Package cmp determines equality of values. ++ Package cmp determines equality of value. + + This package is intended to be a more powerful and safer alternative to + ... // 6 identical lines + For example, an equality function may report floats as equal so long as they + are within some tolerance of each other. +- +- • Types that have an Equal method may use that method to determine equality. +- This allows package authors to determine the equality operation for the types +- that they define. + + • If no custom equality functions are used and no Equal method is defined, + ... // 3 identical lines + by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared + using the AllowUnexported option. +- + """ + ), + StringB: "", + BytesA: nil, + ... // 11 identical fields + } +>>> TestDiff/Reporter/MultilineString +<<< TestDiff/Reporter/Slices + cmp_test.MyComposite{ + StringA: "", + StringB: "", +- BytesA: []uint8{0x01, 0x02, 0x03}, ++ BytesA: nil, +- BytesB: []cmp_test.MyByte{0x04, 0x05, 0x06}, ++ BytesB: nil, +- BytesC: cmp_test.MyBytes{0x07, 0x08, 0x09}, ++ BytesC: nil, +- IntsA: []int8{-1, -2, -3}, ++ IntsA: nil, +- IntsB: []cmp_test.MyInt{-4, -5, -6}, ++ IntsB: nil, +- IntsC: cmp_test.MyInts{-7, -8, -9}, ++ IntsC: nil, +- UintsA: []uint16{1000, 2000, 3000}, ++ UintsA: nil, +- UintsB: []cmp_test.MyUint{4000, 5000, 6000}, ++ UintsB: nil, +- UintsC: cmp_test.MyUints{7000, 8000, 9000}, ++ UintsC: nil, +- FloatsA: []float32{1.5, 2.5, 3.5}, ++ FloatsA: nil, +- FloatsB: []cmp_test.MyFloat{4.5, 5.5, 6.5}, ++ FloatsB: nil, +- FloatsC: cmp_test.MyFloats{7.5, 8.5, 9.5}, ++ FloatsC: nil, + } +>>> TestDiff/Reporter/Slices +<<< TestDiff/Reporter/EmptySlices + cmp_test.MyComposite{ + StringA: "", + StringB: "", +- BytesA: []uint8{}, ++ BytesA: nil, +- BytesB: []cmp_test.MyByte{}, ++ BytesB: nil, +- BytesC: cmp_test.MyBytes{}, ++ BytesC: nil, +- IntsA: []int8{}, ++ IntsA: nil, +- IntsB: []cmp_test.MyInt{}, ++ IntsB: nil, +- IntsC: cmp_test.MyInts{}, ++ IntsC: nil, +- UintsA: []uint16{}, ++ UintsA: nil, +- UintsB: []cmp_test.MyUint{}, ++ UintsB: nil, +- UintsC: cmp_test.MyUints{}, ++ UintsC: nil, +- FloatsA: []float32{}, ++ FloatsA: nil, +- FloatsB: []cmp_test.MyFloat{}, ++ FloatsB: nil, +- FloatsC: cmp_test.MyFloats{}, ++ FloatsC: nil, + } +>>> TestDiff/Reporter/EmptySlices +<<< TestDiff/Reporter/LargeMapKey + map[*[]uint8]int{ +- &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, ++ &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, + } +>>> TestDiff/Reporter/LargeMapKey +<<< TestDiff/Reporter/LargeStringInInterface + struct{ X interface{} }{ + X: strings.Join({ + ... // 485 identical bytes + "s mus. Pellentesque mi lorem, consectetur id porttitor id, solli", + "citudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis", +- ".", ++ ",", + }, ""), + } +>>> TestDiff/Reporter/LargeStringInInterface +<<< TestDiff/Reporter/LargeBytesInInterface + struct{ X interface{} }{ + X: bytes.Join({ + ... // 485 identical bytes + "s mus. Pellentesque mi lorem, consectetur id porttitor id, solli", + "citudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis", +- ".", ++ ",", + }, ""), + } +>>> TestDiff/Reporter/LargeBytesInInterface +<<< TestDiff/Reporter/LargeStandaloneString + struct{ X interface{} }{ +- X: [1]string{ +- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis.", +- }, ++ X: [1]string{ ++ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,", ++ }, + } +>>> TestDiff/Reporter/LargeStandaloneString +<<< TestDiff/EmbeddedStruct/ParentStructA/Inequal + teststructs.ParentStructA{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructA/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructB/Inequal + teststructs.ParentStructB{ + PublicStruct: teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructB/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructC/Inequal + teststructs.ParentStructC{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + } +>>> TestDiff/EmbeddedStruct/ParentStructC/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructD/Inequal + teststructs.ParentStructD{ + PublicStruct: teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + } +>>> TestDiff/EmbeddedStruct/ParentStructD/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructE/Inequal + teststructs.ParentStructE{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructE/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructF/Inequal + teststructs.ParentStructF{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, +- Public: 5, ++ Public: 6, +- private: 6, ++ private: 7, + } +>>> TestDiff/EmbeddedStruct/ParentStructF/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructG/Inequal + &teststructs.ParentStructG{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructG/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructH/Inequal + &teststructs.ParentStructH{ + PublicStruct: &teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructH/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructI/Inequal + &teststructs.ParentStructI{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: &teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructI/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructJ/Inequal + &teststructs.ParentStructJ{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: &teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, + Public: teststructs.PublicStruct{ +- Public: 7, ++ Public: 8, +- private: 8, ++ private: 9, + }, + private: teststructs.privateStruct{ +- Public: 5, ++ Public: 6, +- private: 6, ++ private: 7, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructJ/Inequal +<<< TestDiff/EqualMethod/StructB/ValueInequal + teststructs.StructB{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB/ValueInequal +<<< TestDiff/EqualMethod/StructD/ValueInequal + teststructs.StructD{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructD/ValueInequal +<<< TestDiff/EqualMethod/StructE/ValueInequal + teststructs.StructE{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructE/ValueInequal +<<< TestDiff/EqualMethod/StructF/ValueInequal + teststructs.StructF{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructF/ValueInequal +<<< TestDiff/EqualMethod/StructA1/ValueInequal + teststructs.StructA1{ + StructA: {X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA1/ValueInequal +<<< TestDiff/EqualMethod/StructA1/PointerInequal + &teststructs.StructA1{ + StructA: {X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA1/PointerInequal +<<< TestDiff/EqualMethod/StructB1/ValueInequal + teststructs.StructB1{ + StructB: Inverse(Addr, &teststructs.StructB{X: "NotEqual"}), +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB1/ValueInequal +<<< TestDiff/EqualMethod/StructB1/PointerInequal + &teststructs.StructB1{ + StructB: Inverse(Addr, &teststructs.StructB{X: "NotEqual"}), +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB1/PointerInequal +<<< TestDiff/EqualMethod/StructD1/ValueInequal + teststructs.StructD1{ +- StructD: teststructs.StructD{X: "NotEqual"}, ++ StructD: teststructs.StructD{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructD1/ValueInequal +<<< TestDiff/EqualMethod/StructE1/ValueInequal + teststructs.StructE1{ +- StructE: teststructs.StructE{X: "NotEqual"}, ++ StructE: teststructs.StructE{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructE1/ValueInequal +<<< TestDiff/EqualMethod/StructF1/ValueInequal + teststructs.StructF1{ +- StructF: teststructs.StructF{X: "NotEqual"}, ++ StructF: teststructs.StructF{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructF1/ValueInequal +<<< TestDiff/EqualMethod/StructA2/ValueInequal + teststructs.StructA2{ + StructA: &{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA2/ValueInequal +<<< TestDiff/EqualMethod/StructA2/PointerInequal + &teststructs.StructA2{ + StructA: &{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA2/PointerInequal +<<< TestDiff/EqualMethod/StructB2/ValueInequal + teststructs.StructB2{ + StructB: &{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB2/ValueInequal +<<< TestDiff/EqualMethod/StructB2/PointerInequal + &teststructs.StructB2{ + StructB: &{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB2/PointerInequal +<<< TestDiff/EqualMethod/StructNo/Inequal + teststructs.StructNo{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructNo/Inequal +<<< TestDiff/Cycle/PointersInequal + &&⟪ref#0⟫cmp_test.P( +- &⟪ref#0⟫(...), ++ &&⟪ref#0⟫(...), + ) +>>> TestDiff/Cycle/PointersInequal +<<< TestDiff/Cycle/SlicesInequal + cmp_test.S{ +- ⟪ref#0⟫{⟪ref#0⟫(...)}, ++ ⟪ref#1⟫{{⟪ref#1⟫(...)}}, + } +>>> TestDiff/Cycle/SlicesInequal +<<< TestDiff/Cycle/MapsInequal + cmp_test.M⟪ref#0⟫{ +- 0: ⟪ref#0⟫(...), ++ 0: {0: ⟪ref#0⟫(...)}, + } +>>> TestDiff/Cycle/MapsInequal +<<< TestDiff/Cycle/GraphInequalZeroed + map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + }, + }, + }, + }, + "BuzzBarBravo": &⟪ref#3⟫{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + "BuzzBarBravo": &⟪ref#3⟫(...), + }, + }, + }, + }, + }, + }, + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + }, + }, + "Buzz": &⟪ref#2⟫(...), + }, + }, + "BuzzBarBravo": &⟪ref#3⟫{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + "BuzzBarBravo": &⟪ref#3⟫(...), + }, + }, + "Buzz": &⟪ref#2⟫(...), + }, + }, + }, + }, + "Foo": &⟪ref#4⟫{ + Name: "Foo", + Bravos: map[string]*cmp_test.CycleBravo{ + "FooBravo": &{ +- ID: 101, ++ ID: 0, + Name: "FooBravo", + Mods: 100, + Alphas: {"Foo": &⟪ref#4⟫(...)}, + }, + }, + }, + } +>>> TestDiff/Cycle/GraphInequalZeroed +<<< TestDiff/Cycle/GraphInequalStruct + map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ + ID: 102, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, +- Alphas: nil, ++ Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + }, + }, + }, + }, + "BuzzBarBravo": &⟪ref#3⟫{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}}, +- "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo"}, ++ "BuzzBarBravo": &⟪ref#3⟫(...), + }, + }, + }, + }, + }, + }, + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &⟪ref#0⟫{Name: "Bar", Bravos: {"BarBuzzBravo": &⟪ref#1⟫(...), "BuzzBarBravo": &⟪ref#3⟫{ID: 103, Name: "BuzzBarBravo", Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}}}}, "Buzz": &⟪ref#2⟫(...)}}, + "BuzzBarBravo": &⟪ref#3⟫{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, +- Alphas: nil, ++ Alphas: map[string]*cmp_test.CycleAlpha{ ++ "Bar": &⟪ref#0⟫{ ++ Name: "Bar", ++ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &⟪ref#1⟫{...}, "BuzzBarBravo": &⟪ref#3⟫(...)}, ++ }, ++ "Buzz": &⟪ref#2⟫(...), ++ }, + }, + }, + }, + "Foo": &⟪ref#4⟫{Name: "Foo", Bravos: {"FooBravo": &{ID: 101, Name: "FooBravo", Mods: 100, Alphas: {"Foo": &⟪ref#4⟫(...)}}}}, + } +>>> TestDiff/Cycle/GraphInequalStruct +<<< TestDiff/Project1/ProtoInequal + teststructs.Eagle{ + ... // 4 identical fields + Dreamers: nil, + Prong: 0, + Slaps: []teststructs.Slap{ + ... // 2 identical elements + {}, + {}, + { + Name: "", + Desc: "", + DescLong: "", +- Args: s"metadata", ++ Args: s"metadata2", + Tense: 0, + Interval: 0, + ... // 3 identical fields + }, + }, + StateGoverner: "", + PrankRating: "", + ... // 2 identical fields + } +>>> TestDiff/Project1/ProtoInequal +<<< TestDiff/Project1/Inequal + teststructs.Eagle{ + ... // 2 identical fields + Desc: "some description", + DescLong: "", + Dreamers: []teststructs.Dreamer{ + {}, + { + ... // 4 identical fields + ContSlaps: nil, + ContSlapsInterval: 0, + Animal: []interface{}{ + teststructs.Goat{ + Target: "corporation", + Slaps: nil, + FunnyPrank: "", + Immutable: &teststructs.GoatImmutable{ +- ID: "southbay2", ++ ID: "southbay", +- State: &6, ++ State: &5, + Started: s"2009-11-10 23:00:00 +0000 UTC", + Stopped: s"0001-01-01 00:00:00 +0000 UTC", + ... // 1 ignored and 1 identical fields + }, + }, + teststructs.Donkey{}, + }, + Ornamental: false, + Amoeba: 53, + ... // 5 identical fields + }, + }, + Prong: 0, + Slaps: []teststructs.Slap{ + { + ... // 6 identical fields + Homeland: 0, + FunnyPrank: "", + Immutable: &teststructs.SlapImmutable{ + ID: "immutableSlap", + Out: nil, +- MildSlap: false, ++ MildSlap: true, + PrettyPrint: "", + State: nil, + Started: s"2009-11-10 23:00:00 +0000 UTC", + Stopped: s"0001-01-01 00:00:00 +0000 UTC", + LastUpdate: s"0001-01-01 00:00:00 +0000 UTC", + LoveRadius: &teststructs.LoveRadius{ + Summer: &teststructs.SummerLove{ + Summary: &teststructs.SummerLoveSummary{ + Devices: []string{ + "foo", +- "bar", +- "baz", + }, + ChangeType: {1, 2, 3}, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + }, + }, + StateGoverner: "", + PrankRating: "", + ... // 2 identical fields + } +>>> TestDiff/Project1/Inequal +<<< TestDiff/Project2/InequalOrder + teststructs.GermBatch{ + DirtyGerms: map[int32][]*testprotos.Germ{ + 17: {s"germ1"}, + 18: { +- s"germ2", + s"germ3", + s"germ4", ++ s"germ2", + }, + }, + CleanGerms: nil, + GermMap: {13: s"germ13", 21: s"germ21"}, + ... // 7 identical fields + } +>>> TestDiff/Project2/InequalOrder +<<< TestDiff/Project2/Inequal + teststructs.GermBatch{ + DirtyGerms: map[int32][]*testprotos.Germ{ ++ 17: {s"germ1"}, + 18: Inverse(Sort, []*testprotos.Germ{ + s"germ2", + s"germ3", +- s"germ4", + }), + }, + CleanGerms: nil, + GermMap: {13: s"germ13", 21: s"germ21"}, + DishMap: map[int32]*teststructs.Dish{ + 0: &{err: e"EOF"}, +- 1: nil, ++ 1: &{err: e"unexpected EOF"}, + 2: &{pb: s"dish"}, + }, + HasPreviousResult: true, + DirtyID: 10, + CleanID: 0, +- GermStrain: 421, ++ GermStrain: 22, + TotalDirtyGerms: 0, + InfectedAt: s"2009-11-10 23:00:00 +0000 UTC", + } +>>> TestDiff/Project2/Inequal +<<< TestDiff/Project3/Inequal + teststructs.Dirt{ +- table: &teststructs.MockTable{state: []string{"a", "c"}}, ++ table: &teststructs.MockTable{state: []string{"a", "b", "c"}}, + ts: 12345, +- Discord: 554, ++ Discord: 500, +- Proto: testprotos.Dirt(Inverse(λ, s"blah")), ++ Proto: testprotos.Dirt(Inverse(λ, s"proto")), + wizard: map[string]*testprotos.Wizard{ +- "albus": s"dumbledore", +- "harry": s"potter", ++ "harry": s"otter", + }, + sadistic: nil, + lastTime: 54321, + ... // 1 ignored field + } +>>> TestDiff/Project3/Inequal +<<< TestDiff/Project4/Inequal + teststructs.Cartel{ + Headquarter: teststructs.Headquarter{ + id: 5, + location: "moon", + subDivisions: []string{ +- "alpha", + "bravo", + "charlie", + }, + incorporatedDate: s"0001-01-01 00:00:00 +0000 UTC", + metaData: s"metadata", + privateMessage: nil, + publicMessage: []uint8{ + 0x01, + 0x02, +- 0x03, ++ 0x04, +- 0x04, ++ 0x03, + 0x05, + }, + horseBack: "abcdef", + rattle: "", + ... // 5 identical fields + }, + source: "mars", + creationDate: s"0001-01-01 00:00:00 +0000 UTC", + boss: "al capone", + lastCrimeDate: s"0001-01-01 00:00:00 +0000 UTC", + poisons: []*teststructs.Poison{ + &{ +- poisonType: 1, ++ poisonType: 5, + expiration: s"2009-11-10 23:00:00 +0000 UTC", + manufacturer: "acme", + potency: 0, + }, +- &{poisonType: 2, manufacturer: "acme2"}, + }, + } +>>> TestDiff/Project4/Inequal diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/go.mod b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/go.mod new file mode 100644 index 0000000..5391dee --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/go.mod @@ -0,0 +1,5 @@ +module github.com/google/go-cmp + +go 1.8 + +require golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 diff --git a/root/pkg/mod/github.com/google/go-cmp@v0.5.5/go.sum b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/go.sum new file mode 100644 index 0000000..3ab73ea --- /dev/null +++ b/root/pkg/mod/github.com/google/go-cmp@v0.5.5/go.sum @@ -0,0 +1,2 @@ +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/dependabot.yml b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/dependabot.yml new file mode 100644 index 0000000..6151c64 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: / + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/ci.yml b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/ci.yml new file mode 100644 index 0000000..952f872 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: [push] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + go: [ '1.20', '1.19', '1.18', '1.17', '1.16' ] + os: [ ubuntu-latest, macOS-latest, windows-latest ] + name: ${{ matrix.os }} Go ${{ matrix.go }} Tests + steps: + - uses: actions/checkout@v3 + - name: Setup go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go }} + - run: go test diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/codeql-analysis.yml b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..db5c760 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '31 4 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/release.yml b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/release.yml new file mode 100644 index 0000000..e378b78 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.github/workflows/release.yml @@ -0,0 +1,31 @@ +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +name: Upload Release Assets + +jobs: + build: + name: Upload Release Assets + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Generate build files + uses: thatisuday/go-cross-build@v1.0.2 + with: + platforms: 'linux/amd64, linux/ppc64le, darwin/amd64, darwin/arm64, windows/amd64' + package: 'cmd/godotenv' + name: 'godotenv' + compress: 'true' + dest: 'dist' + - name: Publish Binaries + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + release_name: Release ${{ github.ref }} + tag: ${{ github.ref }} + file: dist/* + file_glob: true + overwrite: true diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.gitignore b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/LICENCE b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/README.md b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/autoload/autoload.go b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/autoload/autoload.go new file mode 100644 index 0000000..fbcd2bd --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/autoload/autoload.go @@ -0,0 +1,15 @@ +package autoload + +/* + You can just read the .env file on import just by doing + + import _ "github.com/joho/godotenv/autoload" + + And bob's your mother's brother +*/ + +import "github.com/joho/godotenv" + +func init() { + godotenv.Load() +} diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/cmd/godotenv/cmd.go b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/cmd/godotenv/cmd.go new file mode 100644 index 0000000..2a7b2d8 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/cmd/godotenv/cmd.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "log" + + "strings" + + "github.com/joho/godotenv" +) + +func main() { + var showHelp bool + flag.BoolVar(&showHelp, "h", false, "show help") + var rawEnvFilenames string + flag.StringVar(&rawEnvFilenames, "f", "", "comma separated paths to .env files") + var overload bool + flag.BoolVar(&overload, "o", false, "override existing .env variables") + + flag.Parse() + + usage := ` +Run a process with an env setup from a .env file + +godotenv [-o] [-f ENV_FILE_PATHS] COMMAND_ARGS + +ENV_FILE_PATHS: comma separated paths to .env files +COMMAND_ARGS: command and args you want to run + +example + godotenv -f /path/to/something/.env,/another/path/.env fortune +` + // if no args or -h flag + // print usage and return + args := flag.Args() + if showHelp || len(args) == 0 { + fmt.Println(usage) + return + } + + // load env + var envFilenames []string + if rawEnvFilenames != "" { + envFilenames = strings.Split(rawEnvFilenames, ",") + } + + // take rest of args and "exec" them + cmd := args[0] + cmdArgs := args[1:] + + err := godotenv.Exec(envFilenames, cmd, cmdArgs, overload) + if err != nil { + log.Fatal(err) + } +} diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/comments.env b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/comments.env new file mode 100644 index 0000000..af9781f --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/comments.env @@ -0,0 +1,4 @@ +# Full line comment +foo=bar # baz +bar=foo#baz +baz="foo"#bar diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/equals.env b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/equals.env new file mode 100644 index 0000000..00c9809 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/equals.env @@ -0,0 +1 @@ +export OPTION_A='postgres://localhost:5432/database?sslmode=disable' diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/exported.env b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/exported.env new file mode 100644 index 0000000..5821377 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/exported.env @@ -0,0 +1,2 @@ +export OPTION_A=2 +export OPTION_B='\n' diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/invalid1.env b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/invalid1.env new file mode 100644 index 0000000..38f7e0e --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/invalid1.env @@ -0,0 +1,2 @@ +INVALID LINE +foo=bar diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/plain.env b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/plain.env new file mode 100644 index 0000000..e033366 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/plain.env @@ -0,0 +1,8 @@ +OPTION_A=1 +OPTION_B=2 +OPTION_C= 3 +OPTION_D =4 +OPTION_E = 5 +OPTION_F = +OPTION_G= +OPTION_H=1 2 \ No newline at end of file diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/quoted.env b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/quoted.env new file mode 100644 index 0000000..7958933 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/quoted.env @@ -0,0 +1,19 @@ +OPTION_A='1' +OPTION_B='2' +OPTION_C='' +OPTION_D='\n' +OPTION_E="1" +OPTION_F="2" +OPTION_G="" +OPTION_H="\n" +OPTION_I = "echo 'asd'" +OPTION_J='line 1 +line 2' +OPTION_K='line one +this is \'quoted\' +one more line' +OPTION_L="line 1 +line 2" +OPTION_M="line one +this is \"quoted\" +one more line" diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/substitutions.env b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/substitutions.env new file mode 100644 index 0000000..44337a9 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/fixtures/substitutions.env @@ -0,0 +1,5 @@ +OPTION_A=1 +OPTION_B=${OPTION_A} +OPTION_C=$OPTION_B +OPTION_D=${OPTION_A}${OPTION_B} +OPTION_E=${OPTION_NOT_DEFINED} diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/go.mod b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/go.mod new file mode 100644 index 0000000..126e61d --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/go.mod @@ -0,0 +1,3 @@ +module github.com/joho/godotenv + +go 1.12 diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/godotenv.go b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/godotenv_test.go b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/godotenv_test.go new file mode 100644 index 0000000..edb5781 --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/godotenv_test.go @@ -0,0 +1,575 @@ +package godotenv + +import ( + "bytes" + "fmt" + "os" + "reflect" + "strings" + "testing" +) + +var noopPresets = make(map[string]string) + +func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) { + result, err := Unmarshal(rawEnvLine) + + if err != nil { + t.Errorf("Expected %q to parse as %q: %q, errored %q", rawEnvLine, expectedKey, expectedValue, err) + return + } + if result[expectedKey] != expectedValue { + t.Errorf("Expected '%v' to parse as '%v' => '%v', got %q instead", rawEnvLine, expectedKey, expectedValue, result) + } +} + +func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, envFileName string, expectedValues map[string]string, presets map[string]string) { + // first up, clear the env + os.Clearenv() + + for k, v := range presets { + os.Setenv(k, v) + } + + err := loader(envFileName) + if err != nil { + t.Fatalf("Error loading %v", envFileName) + } + + for k := range expectedValues { + envValue := os.Getenv(k) + v := expectedValues[k] + if envValue != v { + t.Errorf("Mismatch for key '%v': expected '%#v' got '%#v'", k, v, envValue) + } + } +} + +func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) { + err := Load() + pathError := err.(*os.PathError) + if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" { + t.Errorf("Didn't try and open .env by default") + } +} + +func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) { + err := Overload() + pathError := err.(*os.PathError) + if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" { + t.Errorf("Didn't try and open .env by default") + } +} + +func TestLoadFileNotFound(t *testing.T) { + err := Load("somefilethatwillneverexistever.env") + if err == nil { + t.Error("File wasn't found but Load didn't return an error") + } +} + +func TestOverloadFileNotFound(t *testing.T) { + err := Overload("somefilethatwillneverexistever.env") + if err == nil { + t.Error("File wasn't found but Overload didn't return an error") + } +} + +func TestReadPlainEnv(t *testing.T) { + envFileName := "fixtures/plain.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "2", + "OPTION_C": "3", + "OPTION_D": "4", + "OPTION_E": "5", + "OPTION_F": "", + "OPTION_G": "", + "OPTION_H": "1 2", + } + + envMap, err := Read(envFileName) + if err != nil { + t.Error("Error reading file") + } + + if len(envMap) != len(expectedValues) { + t.Error("Didn't get the right size map back") + } + + for key, value := range expectedValues { + if envMap[key] != value { + t.Error("Read got one of the keys wrong") + } + } +} + +func TestParse(t *testing.T) { + envMap, err := Parse(bytes.NewReader([]byte("ONE=1\nTWO='2'\nTHREE = \"3\""))) + expectedValues := map[string]string{ + "ONE": "1", + "TWO": "2", + "THREE": "3", + } + if err != nil { + t.Fatalf("error parsing env: %v", err) + } + for key, value := range expectedValues { + if envMap[key] != value { + t.Errorf("expected %s to be %s, got %s", key, value, envMap[key]) + } + } +} + +func TestLoadDoesNotOverride(t *testing.T) { + envFileName := "fixtures/plain.env" + + // ensure NO overload + presets := map[string]string{ + "OPTION_A": "do_not_override", + "OPTION_B": "", + } + + expectedValues := map[string]string{ + "OPTION_A": "do_not_override", + "OPTION_B": "", + } + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets) +} + +func TestOverloadDoesOverride(t *testing.T) { + envFileName := "fixtures/plain.env" + + // ensure NO overload + presets := map[string]string{ + "OPTION_A": "do_not_override", + } + + expectedValues := map[string]string{ + "OPTION_A": "1", + } + loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets) +} + +func TestLoadPlainEnv(t *testing.T) { + envFileName := "fixtures/plain.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "2", + "OPTION_C": "3", + "OPTION_D": "4", + "OPTION_E": "5", + "OPTION_H": "1 2", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestLoadExportedEnv(t *testing.T) { + envFileName := "fixtures/exported.env" + expectedValues := map[string]string{ + "OPTION_A": "2", + "OPTION_B": "\\n", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestLoadEqualsEnv(t *testing.T) { + envFileName := "fixtures/equals.env" + expectedValues := map[string]string{ + "OPTION_A": "postgres://localhost:5432/database?sslmode=disable", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestLoadQuotedEnv(t *testing.T) { + envFileName := "fixtures/quoted.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "2", + "OPTION_C": "", + "OPTION_D": "\\n", + "OPTION_E": "1", + "OPTION_F": "2", + "OPTION_G": "", + "OPTION_H": "\n", + "OPTION_I": "echo 'asd'", + "OPTION_J": "line 1\nline 2", + "OPTION_K": "line one\nthis is \\'quoted\\'\none more line", + "OPTION_L": "line 1\nline 2", + "OPTION_M": "line one\nthis is \"quoted\"\none more line", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestSubstitutions(t *testing.T) { + envFileName := "fixtures/substitutions.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "1", + "OPTION_C": "1", + "OPTION_D": "11", + "OPTION_E": "", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestExpanding(t *testing.T) { + tests := []struct { + name string + input string + expected map[string]string + }{ + { + "expands variables found in values", + "FOO=test\nBAR=$FOO", + map[string]string{"FOO": "test", "BAR": "test"}, + }, + { + "parses variables wrapped in brackets", + "FOO=test\nBAR=${FOO}bar", + map[string]string{"FOO": "test", "BAR": "testbar"}, + }, + { + "expands undefined variables to an empty string", + "BAR=$FOO", + map[string]string{"BAR": ""}, + }, + { + "expands variables in double quoted strings", + "FOO=test\nBAR=\"quote $FOO\"", + map[string]string{"FOO": "test", "BAR": "quote test"}, + }, + { + "does not expand variables in single quoted strings", + "BAR='quote $FOO'", + map[string]string{"BAR": "quote $FOO"}, + }, + { + "does not expand escaped variables", + `FOO="foo\$BAR"`, + map[string]string{"FOO": "foo$BAR"}, + }, + { + "does not expand escaped variables", + `FOO="foo\${BAR}"`, + map[string]string{"FOO": "foo${BAR}"}, + }, + { + "does not expand escaped variables", + "FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"", + map[string]string{"FOO": "test", "BAR": "foo${FOO} test"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env, err := Parse(strings.NewReader(tt.input)) + if err != nil { + t.Errorf("Error: %s", err.Error()) + } + for k, v := range tt.expected { + if strings.Compare(env[k], v) != 0 { + t.Errorf("Expected: %s, Actual: %s", v, env[k]) + } + } + }) + } +} + +func TestVariableStringValueSeparator(t *testing.T) { + input := "TEST_URLS=\"stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443\"" + want := map[string]string{ + "TEST_URLS": "stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443", + } + got, err := Parse(strings.NewReader(input)) + if err != nil { + t.Error(err) + } + + if len(got) != len(want) { + t.Fatalf( + "unexpected value:\nwant:\n\t%#v\n\ngot:\n\t%#v", want, got) + } + + for k, wantVal := range want { + gotVal, ok := got[k] + if !ok { + t.Fatalf("key %q doesn't present in result", k) + } + if wantVal != gotVal { + t.Fatalf( + "mismatch in %q value:\nwant:\n\t%s\n\ngot:\n\t%s", k, + wantVal, gotVal) + } + } +} + +func TestActualEnvVarsAreLeftAlone(t *testing.T) { + os.Clearenv() + os.Setenv("OPTION_A", "actualenv") + _ = Load("fixtures/plain.env") + + if os.Getenv("OPTION_A") != "actualenv" { + t.Error("An ENV var set earlier was overwritten") + } +} + +func TestParsing(t *testing.T) { + // unquoted values + parseAndCompare(t, "FOO=bar", "FOO", "bar") + + // parses values with spaces around equal sign + parseAndCompare(t, "FOO =bar", "FOO", "bar") + parseAndCompare(t, "FOO= bar", "FOO", "bar") + + // parses double quoted values + parseAndCompare(t, `FOO="bar"`, "FOO", "bar") + + // parses single quoted values + parseAndCompare(t, "FOO='bar'", "FOO", "bar") + + // parses escaped double quotes + parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`) + + // parses single quotes inside double quotes + parseAndCompare(t, `FOO="'d'"`, "FOO", `'d'`) + + // parses yaml style options + parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1") + + //parses yaml values with equal signs + parseAndCompare(t, "OPTION_A: Foo=bar", "OPTION_A", "Foo=bar") + + // parses non-yaml options with colons + parseAndCompare(t, "OPTION_A=1:B", "OPTION_A", "1:B") + + // parses export keyword + parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2") + parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\\n") + parseAndCompare(t, "export exportFoo=2", "exportFoo", "2") + parseAndCompare(t, "exportFOO=2", "exportFOO", "2") + parseAndCompare(t, "export_FOO =2", "export_FOO", "2") + parseAndCompare(t, "export.FOO= 2", "export.FOO", "2") + parseAndCompare(t, "export\tOPTION_A=2", "OPTION_A", "2") + parseAndCompare(t, " export OPTION_A=2", "OPTION_A", "2") + parseAndCompare(t, "\texport OPTION_A=2", "OPTION_A", "2") + + // it 'expands newlines in quoted strings' do + // expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz") + parseAndCompare(t, `FOO="bar\nbaz"`, "FOO", "bar\nbaz") + + // it 'parses variables with "." in the name' do + // expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar') + parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar") + + // it 'parses variables with several "=" in the value' do + // expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=') + parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=") + + // it 'strips unquoted values' do + // expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar ' + parseAndCompare(t, "FOO=bar ", "FOO", "bar") + + // unquoted internal whitespace is preserved + parseAndCompare(t, `KEY=value value`, "KEY", "value value") + + // it 'ignores inline comments' do + // expect(env("foo=bar # this is foo")).to eql('foo' => 'bar') + parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar") + + // it 'allows # in quoted value' do + // expect(env('foo="bar#baz" # comment')).to eql('foo' => 'bar#baz') + parseAndCompare(t, `FOO="bar#baz" # comment`, "FOO", "bar#baz") + parseAndCompare(t, "FOO='bar#baz' # comment", "FOO", "bar#baz") + parseAndCompare(t, `FOO="bar#baz#bang" # comment`, "FOO", "bar#baz#bang") + + // it 'parses # in quoted values' do + // expect(env('foo="ba#r"')).to eql('foo' => 'ba#r') + // expect(env("foo='ba#r'")).to eql('foo' => 'ba#r') + parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r") + parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r") + + //newlines and backslashes should be escaped + parseAndCompare(t, `FOO="bar\n\ b\az"`, "FOO", "bar\n baz") + parseAndCompare(t, `FOO="bar\\\n\ b\az"`, "FOO", "bar\\\n baz") + parseAndCompare(t, `FOO="bar\\r\ b\az"`, "FOO", "bar\\r baz") + + parseAndCompare(t, `="value"`, "", "value") + + // unquoted whitespace around keys should be ignored + parseAndCompare(t, " KEY =value", "KEY", "value") + parseAndCompare(t, " KEY=value", "KEY", "value") + parseAndCompare(t, "\tKEY=value", "KEY", "value") + + // it 'throws an error if line format is incorrect' do + // expect{env('lol$wut')}.to raise_error(Dotenv::FormatError) + badlyFormattedLine := "lol$wut" + _, err := Unmarshal(badlyFormattedLine) + if err == nil { + t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine) + } +} + +func TestLinesToIgnore(t *testing.T) { + cases := map[string]struct { + input string + want string + }{ + "Line with nothing but line break": { + input: "\n", + }, + "Line with nothing but windows-style line break": { + input: "\r\n", + }, + "Line full of whitespace": { + input: "\t\t ", + }, + "Comment": { + input: "# Comment", + }, + "Indented comment": { + input: "\t # comment", + }, + "non-ignored value": { + input: `export OPTION_B='\n'`, + want: `export OPTION_B='\n'`, + }, + } + + for n, c := range cases { + t.Run(n, func(t *testing.T) { + got := string(getStatementStart([]byte(c.input))) + if got != c.want { + t.Errorf("Expected:\t %q\nGot:\t %q", c.want, got) + } + }) + } +} + +func TestErrorReadDirectory(t *testing.T) { + envFileName := "fixtures/" + envMap, err := Read(envFileName) + + if err == nil { + t.Errorf("Expected error, got %v", envMap) + } +} + +func TestErrorParsing(t *testing.T) { + envFileName := "fixtures/invalid1.env" + envMap, err := Read(envFileName) + if err == nil { + t.Errorf("Expected error, got %v", envMap) + } +} + +func TestComments(t *testing.T) { + envFileName := "fixtures/comments.env" + expectedValues := map[string]string{ + "foo": "bar", + "bar": "foo#baz", + "baz": "foo", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestWrite(t *testing.T) { + writeAndCompare := func(env string, expected string) { + envMap, _ := Unmarshal(env) + actual, _ := Marshal(envMap) + if expected != actual { + t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual) + } + } + //just test some single lines to show the general idea + //TestRoundtrip makes most of the good assertions + + //values are always double-quoted + writeAndCompare(`key=value`, `key="value"`) + //double-quotes are escaped + writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`) + //but single quotes are left alone + writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`) + // newlines, backslashes, and some other special chars are escaped + writeAndCompare(`foo="\n\r\\r!"`, `foo="\n\r\\r\!"`) + // lines should be sorted + writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"") + // integers should not be quoted + writeAndCompare(`key="10"`, `key=10`) + +} + +func TestRoundtrip(t *testing.T) { + fixtures := []string{"equals.env", "exported.env", "plain.env", "quoted.env"} + for _, fixture := range fixtures { + fixtureFilename := fmt.Sprintf("fixtures/%s", fixture) + env, err := readFile(fixtureFilename) + if err != nil { + t.Errorf("Expected '%s' to read without error (%v)", fixtureFilename, err) + } + rep, err := Marshal(env) + if err != nil { + t.Errorf("Expected '%s' to Marshal (%v)", fixtureFilename, err) + } + roundtripped, err := Unmarshal(rep) + if err != nil { + t.Errorf("Expected '%s' to Mashal and Unmarshal (%v)", fixtureFilename, err) + } + if !reflect.DeepEqual(env, roundtripped) { + t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped) + } + + } +} + +func TestTrailingNewlines(t *testing.T) { + cases := map[string]struct { + input string + key string + value string + }{ + "Simple value without trailing newline": { + input: "KEY=value", + key: "KEY", + value: "value", + }, + "Value with internal whitespace without trailing newline": { + input: "KEY=value value", + key: "KEY", + value: "value value", + }, + "Value with internal whitespace with trailing newline": { + input: "KEY=value value\n", + key: "KEY", + value: "value value", + }, + "YAML style - value with internal whitespace without trailing newline": { + input: "KEY: value value", + key: "KEY", + value: "value value", + }, + "YAML style - value with internal whitespace with trailing newline": { + input: "KEY: value value\n", + key: "KEY", + value: "value value", + }, + } + + for n, c := range cases { + t.Run(n, func(t *testing.T) { + result, err := Unmarshal(c.input) + if err != nil { + t.Errorf("Input: %q Unexpected error:\t%q", c.input, err) + } + if result[c.key] != c.value { + t.Errorf("Input %q Expected:\t %q/%q\nGot:\t %q", c.input, c.key, c.value, result) + } + }) + } +} diff --git a/root/pkg/mod/github.com/joho/godotenv@v1.5.1/parser.go b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/root/pkg/mod/github.com/joho/godotenv@v1.5.1/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/.circleci/config.yml b/root/pkg/mod/gotest.tools/v3@v3.3.0/.circleci/config.yml new file mode 100644 index 0000000..f5ad793 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/.circleci/config.yml @@ -0,0 +1,63 @@ +version: 2.1 + +orbs: + go: gotest/tools@0.0.14 + +workflows: + ci: + jobs: + - lint + - go/test: + name: test-golang-1.15 + executor: + name: go/golang + tag: 1.15-alpine + - go/test: + name: test-golang-1.16 + executor: + name: go/golang + tag: 1.16-alpine + - go/test: + name: test-golang-1.17 + executor: + name: go/golang + tag: 1.17-alpine + - go/test: + name: test-golang-1.18 + executor: + name: go/golang + tag: 1.18-alpine + - go/test: + name: test-windows + executor: windows + pre-steps: + - run: | + git config --global core.autocrlf false + git config --global core.symlinks true + - run: | + choco upgrade golang + echo 'export PATH="$PATH:/c/Program Files/Go/bin"' > $BASH_ENV + - run: go version + +executors: + windows: + machine: + image: windows-server-2019-vs2019:stable + resource_class: windows.medium + shell: bash.exe + +jobs: + + lint: + executor: + name: go/golang + tag: 1.18-alpine + steps: + - checkout + - go/install-golangci-lint: + prefix: v1.45.2 + version: 1.45.2 + - go/install: {package: git} + - run: + name: Lint + command: golangci-lint run -v --concurrency 2 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/.codecov.yml b/root/pkg/mod/gotest.tools/v3@v3.3.0/.codecov.yml new file mode 100644 index 0000000..4dd7324 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/.codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + threshold: 2 + patch: + default: + threshold: 20 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/.github/workflows/sync-main.yaml b/root/pkg/mod/gotest.tools/v3@v3.3.0/.github/workflows/sync-main.yaml new file mode 100644 index 0000000..3f052d0 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/.github/workflows/sync-main.yaml @@ -0,0 +1,18 @@ +name: Merge main into master + +on: + push: + branches: [main] + +jobs: + sync: + name: Merge main branch + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: {fetch-depth: 0} + - name: merge + run: | + git checkout master + git merge --ff-only main + git push origin master diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/.gitignore b/root/pkg/mod/gotest.tools/v3@v3.3.0/.gitignore new file mode 100644 index 0000000..97a6510 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/.gitignore @@ -0,0 +1,5 @@ +vendor/ +.dobi/ +Gopkg.lock +.depsources +dist/ diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/.golangci.yml b/root/pkg/mod/gotest.tools/v3@v3.3.0/.golangci.yml new file mode 100644 index 0000000..139e1af --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/.golangci.yml @@ -0,0 +1,68 @@ +linters-settings: + goconst: + min-len: 5 + min-occurrences: 10 + lll: + line-length: 100 + maintidx: + under: 40 + +issues: + exclude-use-default: false + exclude-rules: + - text: 'result .* is always' + linters: [unparam] + - text: 'always receives' + linters: [unparam] + - path: _test\.go + linters: [errcheck, staticcheck, lll, maintidx] + - path: internal/difflib/difflib\.go + text: . + - text: 'return value of .*Close` is not checked' + linters: [errcheck] + - text: 'SA1019' + linters: [staticcheck] + - path: internal/ + text: 'ST1000' + linters: [stylecheck] + - path: 'example_test\.go' + linters: [bodyclose] + +linters: + disable-all: true + enable: + - bodyclose + - deadcode + - depguard + - dogsled + - errcheck + - errorlint + - exportloopref + - gocognit + - goconst + - gofmt + - goimports + - golint + - gosimple + - govet + - ineffassign + - interfacer + - lll + - maintidx + - misspell + - nakedret + - nestif + - nilerr + - nilnil + - nolintlint + - prealloc + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + - wastedassign + - whitespace diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/CONTRIBUTING.md b/root/pkg/mod/gotest.tools/v3@v3.3.0/CONTRIBUTING.md new file mode 100644 index 0000000..d157c77 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing to gotest.tools + +Thank you for your interest in contributing to the project! Below are some +suggestions which may make the process easier. + +## Pull requests + +Pull requests for new features should generally be preceded by an issue +explaining the feature and why it is necessary. + +Pull requests for bug fixes are always appreciated. They should almost always +include a test which fails without the bug fix. diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/Dockerfile b/root/pkg/mod/gotest.tools/v3@v3.3.0/Dockerfile new file mode 100644 index 0000000..72942fd --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/Dockerfile @@ -0,0 +1,33 @@ + +ARG GOLANG_VERSION +FROM golang:${GOLANG_VERSION:-1.12-alpine} as golang +RUN apk add -U curl git bash +WORKDIR /go/src/gotest.tools +ENV CGO_ENABLED=0 \ + PS1="# " \ + GO111MODULE=on + +FROM golang as tools +RUN go get github.com/dnephin/filewatcher@v0.3.2 + +ARG DEP_TAG=v0.4.1 +RUN export GO111MODULE=off; \ + go get -d github.com/golang/dep/cmd/dep && \ + cd /go/src/github.com/golang/dep && \ + git checkout -q "$DEP_TAG" && \ + go build -o /usr/bin/dep ./cmd/dep + +RUN go get gotest.tools/gotestsum@v0.3.3 +RUN wget -O- -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s && \ + mv bin/golangci-lint /go/bin + + +FROM golang as dev +COPY --from=tools /go/bin/filewatcher /usr/bin/filewatcher +COPY --from=tools /usr/bin/dep /usr/bin/dep +COPY --from=tools /go/bin/gotestsum /usr/bin/gotestsum +COPY --from=tools /go/bin/golangci-lint /usr/bin/golangci-lint + + +FROM dev as dev-with-source +COPY . . diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/LICENSE b/root/pkg/mod/gotest.tools/v3@v3.3.0/LICENSE new file mode 100644 index 0000000..aeaa2fa --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/LICENSE @@ -0,0 +1,13 @@ +Copyright 2018 gotest.tools authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/README.md b/root/pkg/mod/gotest.tools/v3@v3.3.0/README.md new file mode 100644 index 0000000..d9876aa --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/README.md @@ -0,0 +1,53 @@ +# gotest.tools + +A collection of packages to augment `testing` and support common patterns. + +[![GoDoc](https://godoc.org/gotest.tools?status.svg)](https://pkg.go.dev/gotest.tools/v3/?tab=subdirectories) +[![CircleCI](https://circleci.com/gh/gotestyourself/gotest.tools/tree/main.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotest.tools/tree/main) +[![Go Reportcard](https://goreportcard.com/badge/gotest.tools)](https://goreportcard.com/report/gotest.tools) + +## Usage + +With Go modules enabled (go1.11+) + +``` +$ go get gotest.tools/v3 +``` + +``` +import "gotest.tools/v3/assert" +``` + +To use `gotest.tools` with an older version of Go that does not understand Go +module paths pin to version `v2.3.0`. + + +## Packages + +* [assert](http://pkg.go.dev/gotest.tools/v3/assert) - + compare values and fail the test when a comparison fails +* [env](http://pkg.go.dev/gotest.tools/v3/env) - + test code which uses environment variables +* [fs](http://pkg.go.dev/gotest.tools/v3/fs) - + create temporary files and compare a filesystem tree to an expected value +* [golden](http://pkg.go.dev/gotest.tools/v3/golden) - + compare large multi-line strings against values frozen in golden files +* [icmd](http://pkg.go.dev/gotest.tools/v3/icmd) - + execute binaries and test the output +* [poll](http://pkg.go.dev/gotest.tools/v3/poll) - + test asynchronous code by polling until a desired state is reached +* [skip](http://pkg.go.dev/gotest.tools/v3/skip) - + skip a test and print the source code of the condition used to skip the test + +## Related + +* [gotest.tools/gotestsum](https://github.com/gotestyourself/gotestsum) - + go test runner with custom output +* [go testing patterns](https://github.com/gotestyourself/gotest.tools/wiki/Go-Testing-Patterns) - + zero-depedency patterns for organizing test cases +* [test doubles and patching](https://github.com/gotestyourself/gotest.tools/wiki/Test-Doubles-And-Patching) - + zero-depdency test doubles (fakes, spies, stubs, and mocks) and monkey patching patterns + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert.go new file mode 100644 index 0000000..dbd4f5a --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert.go @@ -0,0 +1,314 @@ +/*Package assert provides assertions for comparing expected values to actual +values in tests. When an assertion fails a helpful error message is printed. + +Example usage + +All the assertions in this package use testing.T.Helper to mark themselves as +test helpers. This allows the testing package to print the filename and line +number of the file function that failed. + + assert.NilError(t, err) + // filename_test.go:212: assertion failed: error is not nil: file not found + +If any assertion is called from a helper function, make sure to call t.Helper +from the helper function so that the filename and line number remain correct. + +The examples below show assert used with some common types and the failure +messages it produces. The filename and line number portion of the failure +message is omitted from these examples for brevity. + + // booleans + + assert.Assert(t, ok) + // assertion failed: ok is false + assert.Assert(t, !missing) + // assertion failed: missing is true + + // primitives + + assert.Equal(t, count, 1) + // assertion failed: 0 (count int) != 1 (int) + assert.Equal(t, msg, "the message") + // assertion failed: my message (msg string) != the message (string) + assert.Assert(t, total != 10) // use Assert for NotEqual + // assertion failed: total is 10 + assert.Assert(t, count > 20, "count=%v", count) + // assertion failed: count is <= 20: count=1 + + // errors + + assert.NilError(t, closer.Close()) + // assertion failed: error is not nil: close /file: errno 11 + assert.Error(t, err, "the exact error message") + // assertion failed: expected error "the exact error message", got "oops" + assert.ErrorContains(t, err, "includes this") + // assertion failed: expected error to contain "includes this", got "oops" + assert.ErrorIs(t, err, os.ErrNotExist) + // assertion failed: error is "oops", not "file does not exist" (os.ErrNotExist) + + // complex types + + assert.DeepEqual(t, result, myStruct{Name: "title"}) + // assertion failed: ... (diff of the two structs) + assert.Assert(t, is.Len(items, 3)) + // assertion failed: expected [] (length 0) to have length 3 + assert.Assert(t, len(sequence) != 0) // use Assert for NotEmpty + // assertion failed: len(sequence) is 0 + assert.Assert(t, is.Contains(mapping, "key")) + // assertion failed: map[other:1] does not contain key + + // pointers and interface + + assert.Assert(t, ref == nil) + // assertion failed: ref is not nil + assert.Assert(t, ref != nil) // use Assert for NotNil + // assertion failed: ref is nil + +Assert and Check + +Assert and Check are very similar, they both accept a Comparison, and fail +the test when the comparison fails. The one difference is that Assert uses +testing.T.FailNow to fail the test, which will end the test execution immediately. +Check uses testing.T.Fail to fail the test, which allows it to return the +result of the comparison, then proceed with the rest of the test case. + +Like testing.T.FailNow, Assert must be called from the goroutine running the test, +not from other goroutines created during the test. Check is safe to use from any +goroutine. + +Comparisons + +Package http://pkg.go.dev/gotest.tools/v3/assert/cmp provides +many common comparisons. Additional comparisons can be written to compare +values in other ways. See the example Assert (CustomComparison). + +Automated migration from testify + +gty-migrate-from-testify is a command which translates Go source code from +testify assertions to the assertions provided by this package. + +See http://pkg.go.dev/gotest.tools/v3/assert/cmd/gty-migrate-from-testify. + + +*/ +package assert // import "gotest.tools/v3/assert" + +import ( + gocmp "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/assert" +) + +// BoolOrComparison can be a bool, cmp.Comparison, or error. See Assert for +// details about how this type is used. +type BoolOrComparison interface{} + +// TestingT is the subset of testing.T used by the assert package. +type TestingT interface { + FailNow() + Fail() + Log(args ...interface{}) +} + +type helperT interface { + Helper() +} + +// Assert performs a comparison. If the comparison fails, the test is marked as +// failed, a failure message is logged, and execution is stopped immediately. +// +// The comparison argument may be one of three types: +// +// bool +// True is success. False is a failure. The failure message will contain +// the literal source code of the expression. +// +// cmp.Comparison +// Uses cmp.Result.Success() to check for success or failure. +// The comparison is responsible for producing a helpful failure message. +// http://pkg.go.dev/gotest.tools/v3/assert/cmp provides many common comparisons. +// +// error +// A nil value is considered success, and a non-nil error is a failure. +// The return value of error.Error is used as the failure message. +// +// +// Extra details can be added to the failure message using msgAndArgs. msgAndArgs +// may be either a single string, or a format string and args that will be +// passed to fmt.Sprintf. +// +// Assert uses t.FailNow to fail the test. Like t.FailNow, Assert must be called +// from the goroutine running the test function, not from other +// goroutines created during the test. Use Check from other goroutines. +func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsFromComparisonCall, comparison, msgAndArgs...) { + t.FailNow() + } +} + +// Check performs a comparison. If the comparison fails the test is marked as +// failed, a failure message is printed, and Check returns false. If the comparison +// is successful Check returns true. Check may be called from any goroutine. +// +// See Assert for details about the comparison arg and failure messages. +func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsFromComparisonCall, comparison, msgAndArgs...) { + t.Fail() + return false + } + return true +} + +// NilError fails the test immediately if err is not nil, and includes err.Error +// in the failure message. +// +// NilError uses t.FailNow to fail the test. Like t.FailNow, NilError must be +// called from the goroutine running the test function, not from other +// goroutines created during the test. Use Check from other goroutines. +func NilError(t TestingT, err error, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, err, msgAndArgs...) { + t.FailNow() + } +} + +// Equal uses the == operator to assert two values are equal and fails the test +// if they are not equal. +// +// If the comparison fails Equal will use the variable names and types of +// x and y as part of the failure message to identify the actual and expected +// values. +// +// assert.Equal(t, actual, expected) +// // main_test.go:41: assertion failed: 1 (actual int) != 21 (expected int32) +// +// If either x or y are a multi-line string the failure message will include a +// unified diff of the two values. If the values only differ by whitespace +// the unified diff will be augmented by replacing whitespace characters with +// visible characters to identify the whitespace difference. +// +// Equal uses t.FailNow to fail the test. Like t.FailNow, Equal must be +// called from the goroutine running the test function, not from other +// goroutines created during the test. Use Check with cmp.Equal from other +// goroutines. +func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.Equal(x, y), msgAndArgs...) { + t.FailNow() + } +} + +// DeepEqual uses google/go-cmp (https://godoc.org/github.com/google/go-cmp/cmp) +// to assert two values are equal and fails the test if they are not equal. +// +// Package http://pkg.go.dev/gotest.tools/v3/assert/opt provides some additional +// commonly used Options. +// +// DeepEqual uses t.FailNow to fail the test. Like t.FailNow, DeepEqual must be +// called from the goroutine running the test function, not from other +// goroutines created during the test. Use Check with cmp.DeepEqual from other +// goroutines. +func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.DeepEqual(x, y, opts...)) { + t.FailNow() + } +} + +// Error fails the test if err is nil, or if err.Error is not equal to expected. +// Both err.Error and expected will be included in the failure message. +// Error performs an exact match of the error text. Use ErrorContains if only +// part of the error message is relevant. Use ErrorType or ErrorIs to compare +// errors by type. +// +// Error uses t.FailNow to fail the test. Like t.FailNow, Error must be +// called from the goroutine running the test function, not from other +// goroutines created during the test. Use Check with cmp.Error from other +// goroutines. +func Error(t TestingT, err error, expected string, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.Error(err, expected), msgAndArgs...) { + t.FailNow() + } +} + +// ErrorContains fails the test if err is nil, or if err.Error does not +// contain the expected substring. Both err.Error and the expected substring +// will be included in the failure message. +// +// ErrorContains uses t.FailNow to fail the test. Like t.FailNow, ErrorContains +// must be called from the goroutine running the test function, not from other +// goroutines created during the test. Use Check with cmp.ErrorContains from other +// goroutines. +func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorContains(err, substring), msgAndArgs...) { + t.FailNow() + } +} + +// ErrorType fails the test if err is nil, or err is not the expected type. +// Most new code should use ErrorIs instead. ErrorType may be deprecated in the +// future. +// +// Expected can be one of: +// +// func(error) bool +// The function should return true if the error is the expected type. +// +// struct{} or *struct{} +// A struct or a pointer to a struct. The assertion fails if the error is +// not of the same type. +// +// *interface{} +// A pointer to an interface type. The assertion fails if err does not +// implement the interface. +// +// reflect.Type +// The assertion fails if err does not implement the reflect.Type. +// +// ErrorType uses t.FailNow to fail the test. Like t.FailNow, ErrorType +// must be called from the goroutine running the test function, not from other +// goroutines created during the test. Use Check with cmp.ErrorType from other +// goroutines. +func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorType(err, expected), msgAndArgs...) { + t.FailNow() + } +} + +// ErrorIs fails the test if err is nil, or the error does not match expected +// when compared using errors.Is. See https://golang.org/pkg/errors/#Is for +// accepted arguments. +// +// ErrorIs uses t.FailNow to fail the test. Like t.FailNow, ErrorIs +// must be called from the goroutine running the test function, not from other +// goroutines created during the test. Use Check with cmp.ErrorIs from other +// goroutines. +func ErrorIs(t TestingT, err error, expected error, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorIs(err, expected), msgAndArgs...) { + t.FailNow() + } +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert_ext_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert_ext_test.go new file mode 100644 index 0000000..5903f70 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert_ext_test.go @@ -0,0 +1,112 @@ +package assert_test + +import ( + "go/parser" + "go/token" + "io/ioutil" + "runtime" + "strings" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/internal/source" +) + +func TestEqual_WithGoldenUpdate(t *testing.T) { + t.Run("assert failed with -update=false", func(t *testing.T) { + ft := &fakeTestingT{} + actual := `not this value` + assert.Equal(ft, actual, expectedOne) + assert.Assert(t, ft.failNowed) + }) + + t.Run("var is updated when -update=true", func(t *testing.T) { + patchUpdate(t) + t.Cleanup(func() { + resetVariable(t, "expectedOne", "") + }) + + actual := `this is the +actual value +that we are testing +` + assert.Equal(t, actual, expectedOne) + + raw, err := ioutil.ReadFile(fileName(t)) + assert.NilError(t, err) + + expected := "var expectedOne = `this is the\nactual value\nthat we are testing\n`" + assert.Assert(t, strings.Contains(string(raw), expected), "actual=%v", string(raw)) + }) + + t.Run("const is updated when -update=true", func(t *testing.T) { + patchUpdate(t) + t.Cleanup(func() { + resetVariable(t, "expectedTwo", "") + }) + + actual := `this is the new +expected value +` + assert.Equal(t, actual, expectedTwo) + + raw, err := ioutil.ReadFile(fileName(t)) + assert.NilError(t, err) + + expected := "const expectedTwo = `this is the new\nexpected value\n`" + assert.Assert(t, strings.Contains(string(raw), expected), "actual=%v", string(raw)) + }) +} + +// expectedOne is updated by running the tests with -update +var expectedOne = `` + +// expectedTwo is updated by running the tests with -update +const expectedTwo = `` + +func patchUpdate(t *testing.T) { + source.Update = true + t.Cleanup(func() { + source.Update = false + }) +} + +func fileName(t *testing.T) string { + t.Helper() + _, filename, _, ok := runtime.Caller(1) + assert.Assert(t, ok, "failed to get call stack") + return filename +} + +func resetVariable(t *testing.T, varName string, value string) { + t.Helper() + _, filename, _, ok := runtime.Caller(1) + assert.Assert(t, ok, "failed to get call stack") + + fileset := token.NewFileSet() + astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors|parser.ParseComments) + assert.NilError(t, err) + + err = source.UpdateVariable(filename, fileset, astFile, varName, value) + assert.NilError(t, err, "failed to reset file") +} + +type fakeTestingT struct { + failNowed bool + failed bool + msgs []string +} + +func (f *fakeTestingT) FailNow() { + f.failNowed = true +} + +func (f *fakeTestingT) Fail() { + f.failed = true +} + +func (f *fakeTestingT) Log(args ...interface{}) { + f.msgs = append(f.msgs, args[0].(string)) +} + +func (f *fakeTestingT) Helper() {} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert_test.go new file mode 100644 index 0000000..e8cd901 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/assert_test.go @@ -0,0 +1,470 @@ +package assert + +import ( + "fmt" + "os" + "testing" + + gocmp "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert/cmp" +) + +type fakeTestingT struct { + failNowed bool + failed bool + msgs []string +} + +func (f *fakeTestingT) FailNow() { + f.failNowed = true +} + +func (f *fakeTestingT) Fail() { + f.failed = true +} + +func (f *fakeTestingT) Log(args ...interface{}) { + f.msgs = append(f.msgs, args[0].(string)) +} + +func (f *fakeTestingT) Helper() {} + +func TestAssert_WithBinaryExpression_Failures(t *testing.T) { + t.Run("equal", func(t *testing.T) { + fakeT := &fakeTestingT{} + Assert(fakeT, 1 == 6) + expectFailNowed(t, fakeT, "assertion failed: 1 is not 6") + }) + t.Run("not equal", func(t *testing.T) { + fakeT := &fakeTestingT{} + a := 1 + Assert(fakeT, a != 1) + expectFailNowed(t, fakeT, "assertion failed: a is 1") + }) + t.Run("greater than", func(t *testing.T) { + fakeT := &fakeTestingT{} + Assert(fakeT, 1 > 5) + expectFailNowed(t, fakeT, "assertion failed: 1 is <= 5") + }) + t.Run("less than", func(t *testing.T) { + fakeT := &fakeTestingT{} + Assert(fakeT, 5 < 1) + expectFailNowed(t, fakeT, "assertion failed: 5 is >= 1") + }) + t.Run("greater than or equal", func(t *testing.T) { + fakeT := &fakeTestingT{} + Assert(fakeT, 1 >= 5) + expectFailNowed(t, fakeT, "assertion failed: 1 is less than 5") + }) + t.Run("less than or equal", func(t *testing.T) { + fakeT := &fakeTestingT{} + Assert(fakeT, 6 <= 2) + expectFailNowed(t, fakeT, "assertion failed: 6 is greater than 2") + }) +} + +func TestAssertWithBoolIdent(t *testing.T) { + fakeT := &fakeTestingT{} + + var ok bool + Assert(fakeT, ok) + expectFailNowed(t, fakeT, "assertion failed: ok is false") +} + +func TestAssertWithBoolFailureNotEqual(t *testing.T) { + fakeT := &fakeTestingT{} + + var err error + Assert(fakeT, err != nil) + expectFailNowed(t, fakeT, "assertion failed: err is nil") +} + +func TestAssertWithBoolFailureNotTrue(t *testing.T) { + fakeT := &fakeTestingT{} + + badNews := true + Assert(fakeT, !badNews) + expectFailNowed(t, fakeT, "assertion failed: badNews is true") +} + +func TestAssertWithBoolFailureAndExtraMessage(t *testing.T) { + fakeT := &fakeTestingT{} + + Assert(fakeT, 1 > 5, "sometimes things fail") + expectFailNowed(t, fakeT, "assertion failed: 1 is <= 5: sometimes things fail") +} + +func TestAssertWithBoolSuccess(t *testing.T) { + fakeT := &fakeTestingT{} + + Assert(fakeT, 1 < 5) + expectSuccess(t, fakeT) +} + +func TestAssertWithBoolMultiLineFailure(t *testing.T) { + fakeT := &fakeTestingT{} + + Assert(fakeT, func() bool { + for range []int{1, 2, 3, 4} { + } + return false + }()) + expectFailNowed(t, fakeT, `assertion failed: expression is false: func() bool { + for range []int{1, 2, 3, 4} { + } + return false +}()`) +} + +type exampleComparison struct { + success bool + message string +} + +func (c exampleComparison) Compare() (bool, string) { + return c.success, c.message +} + +func TestAssertWithComparisonSuccess(t *testing.T) { + fakeT := &fakeTestingT{} + + cmp := exampleComparison{success: true} + Assert(fakeT, cmp.Compare) + expectSuccess(t, fakeT) +} + +func TestAssertWithComparisonFailure(t *testing.T) { + fakeT := &fakeTestingT{} + + cmp := exampleComparison{message: "oops, not good"} + Assert(fakeT, cmp.Compare) + expectFailNowed(t, fakeT, "assertion failed: oops, not good") +} + +func TestAssertWithComparisonAndExtraMessage(t *testing.T) { + fakeT := &fakeTestingT{} + + cmp := exampleComparison{message: "oops, not good"} + Assert(fakeT, cmp.Compare, "extra stuff %v", true) + expectFailNowed(t, fakeT, "assertion failed: oops, not good: extra stuff true") +} + +type customError struct { + field bool +} + +func (e *customError) Error() string { + // access a field of the receiver to simulate the behaviour of most + // implementations, and test handling of non-nil typed errors. + e.field = true + return "custom error" +} + +func TestNilError(t *testing.T) { + t.Run("nil interface", func(t *testing.T) { + fakeT := &fakeTestingT{} + var err error + NilError(fakeT, err) + expectSuccess(t, fakeT) + }) + + t.Run("nil literal", func(t *testing.T) { + fakeT := &fakeTestingT{} + NilError(fakeT, nil) + expectSuccess(t, fakeT) + }) + + t.Run("interface with non-nil type", func(t *testing.T) { + fakeT := &fakeTestingT{} + var customErr *customError + NilError(fakeT, customErr) + expected := "assertion failed: error is not nil: error has type *assert.customError" + expectFailNowed(t, fakeT, expected) + }) + + t.Run("non-nil error", func(t *testing.T) { + fakeT := &fakeTestingT{} + NilError(fakeT, fmt.Errorf("this is the error")) + expectFailNowed(t, fakeT, "assertion failed: error is not nil: this is the error") + }) + + t.Run("non-nil error with struct type", func(t *testing.T) { + fakeT := &fakeTestingT{} + err := structError{} + NilError(fakeT, err) + expectFailNowed(t, fakeT, "assertion failed: error is not nil: this is a struct") + }) + + t.Run("non-nil error with map type", func(t *testing.T) { + fakeT := &fakeTestingT{} + var err mapError + NilError(fakeT, err) + expectFailNowed(t, fakeT, "assertion failed: error is not nil: ") + }) +} + +type structError struct{} + +func (structError) Error() string { + return "this is a struct" +} + +type mapError map[int]string + +func (m mapError) Error() string { + return m[0] +} + +func TestCheckFailure(t *testing.T) { + fakeT := &fakeTestingT{} + + if Check(fakeT, 1 == 2) { + t.Error("expected check to return false on failure") + } + expectFailed(t, fakeT, "assertion failed: 1 is not 2") +} + +func TestCheckSuccess(t *testing.T) { + fakeT := &fakeTestingT{} + + if !Check(fakeT, true) { + t.Error("expected check to return true on success") + } + expectSuccess(t, fakeT) +} + +func TestCheckEqualFailure(t *testing.T) { + fakeT := &fakeTestingT{} + + actual, expected := 5, 9 + Check(fakeT, cmp.Equal(actual, expected)) + expectFailed(t, fakeT, "assertion failed: 5 (actual int) != 9 (expected int)") +} + +func TestCheck_MultipleFunctionsOnTheSameLine(t *testing.T) { + fakeT := &fakeTestingT{} + + f := func(b bool) {} + f(Check(fakeT, false)) + // TODO: update the expected when there is a more correct fix + expectFailed(t, fakeT, + "assertion failed: but assert failed to find the expression to print") +} + +func TestEqualSuccess(t *testing.T) { + fakeT := &fakeTestingT{} + + Equal(fakeT, 1, 1) + expectSuccess(t, fakeT) + + Equal(fakeT, "abcd", "abcd") + expectSuccess(t, fakeT) +} + +func TestEqualFailure(t *testing.T) { + fakeT := &fakeTestingT{} + + actual, expected := 1, 3 + Equal(fakeT, actual, expected) + expectFailNowed(t, fakeT, "assertion failed: 1 (actual int) != 3 (expected int)") +} + +func TestEqualFailureTypes(t *testing.T) { + fakeT := &fakeTestingT{} + + Equal(fakeT, 3, uint(3)) + expectFailNowed(t, fakeT, `assertion failed: 3 (int) != 3 (uint)`) +} + +func TestEqualFailureWithSelectorArgument(t *testing.T) { + fakeT := &fakeTestingT{} + + type tc struct { + expected string + } + var testcase = tc{expected: "foo"} + + Equal(fakeT, "ok", testcase.expected) + expectFailNowed(t, fakeT, + "assertion failed: ok (string) != foo (testcase.expected string)") +} + +func TestEqualFailureWithIndexExpr(t *testing.T) { + fakeT := &fakeTestingT{} + + expected := map[string]string{"foo": "bar"} + Equal(fakeT, "ok", expected["foo"]) + expectFailNowed(t, fakeT, + `assertion failed: ok (string) != bar (expected["foo"] string)`) +} + +func TestEqualFailureWithCallExprArgument(t *testing.T) { + fakeT := &fakeTestingT{} + ce := customError{} + Equal(fakeT, "", ce.Error()) + expectFailNowed(t, fakeT, + "assertion failed: (string) != custom error (string)") +} + +func TestAssertFailureWithOfflineComparison(t *testing.T) { + fakeT := &fakeTestingT{} + a := 1 + b := 2 + // store comparison in a variable, so ast lookup can't find it + comparison := cmp.Equal(a, b) + Assert(fakeT, comparison) + // expected value wont have variable names + expectFailNowed(t, fakeT, "assertion failed: 1 (int) != 2 (int)") +} + +type testingT interface { + Errorf(msg string, args ...interface{}) + Fatalf(msg string, args ...interface{}) +} + +func expectFailNowed(t testingT, fakeT *fakeTestingT, expected string) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if fakeT.failed { + t.Errorf("should not have failed, got messages %s", fakeT.msgs) + } + if !fakeT.failNowed { + t.Fatalf("should have failNowed with message %s", expected) + } + if fakeT.msgs[0] != expected { + t.Fatalf("should have failure message %q, got %q", expected, fakeT.msgs[0]) + } +} + +func expectFailed(t testingT, fakeT *fakeTestingT, expected string) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if fakeT.failNowed { + t.Errorf("should not have failNowed, got messages %s", fakeT.msgs) + } + if !fakeT.failed { + t.Fatalf("should have failed with message %s", expected) + } + if fakeT.msgs[0] != expected { + t.Fatalf("should have failure message %q, got %q", expected, fakeT.msgs[0]) + } +} + +func expectSuccess(t testingT, fakeT *fakeTestingT) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if fakeT.failNowed { + t.Errorf("should not have failNowed, got messages %s", fakeT.msgs) + } + if fakeT.failed { + t.Errorf("should not have failed, got messages %s", fakeT.msgs) + } +} + +type stub struct { + a string + b int +} + +func TestDeepEqualSuccess(t *testing.T) { + actual := stub{"ok", 1} + expected := stub{"ok", 1} + + fakeT := &fakeTestingT{} + DeepEqual(fakeT, actual, expected, gocmp.AllowUnexported(stub{})) + expectSuccess(t, fakeT) +} + +func TestDeepEqualFailure(t *testing.T) { + actual := stub{"ok", 1} + expected := stub{"ok", 2} + + fakeT := &fakeTestingT{} + DeepEqual(fakeT, actual, expected, gocmp.AllowUnexported(stub{})) + if !fakeT.failNowed { + t.Fatal("should have failNowed") + } +} + +func TestErrorFailure(t *testing.T) { + t.Run("nil error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + var err error + Error(fakeT, err, "this error") + expectFailNowed(t, fakeT, "assertion failed: expected an error, got nil") + }) + t.Run("different error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + err := fmt.Errorf("the actual error") + Error(fakeT, err, "this error") + expected := `assertion failed: expected error "this error", got "the actual error"` + expectFailNowed(t, fakeT, expected) + }) +} + +func TestErrorContainsFailure(t *testing.T) { + t.Run("nil error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + var err error + ErrorContains(fakeT, err, "this error") + expectFailNowed(t, fakeT, "assertion failed: expected an error, got nil") + }) + t.Run("different error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + err := fmt.Errorf("the actual error") + ErrorContains(fakeT, err, "this error") + expected := `assertion failed: expected error to contain "this error", got "the actual error"` + expectFailNowed(t, fakeT, expected) + }) +} + +func TestErrorTypeFailure(t *testing.T) { + t.Run("nil error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + var err error + ErrorType(fakeT, err, os.IsNotExist) + expectFailNowed(t, fakeT, "assertion failed: error is nil, not os.IsNotExist") + }) + t.Run("different error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + err := fmt.Errorf("the actual error") + ErrorType(fakeT, err, os.IsNotExist) + expected := `assertion failed: error is the actual error (*errors.errorString), not os.IsNotExist` + expectFailNowed(t, fakeT, expected) + }) +} + +func TestErrorIs(t *testing.T) { + t.Run("nil error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + var err error + ErrorIs(fakeT, err, os.ErrNotExist) + expected := `assertion failed: error is nil, not "file does not exist" (os.ErrNotExist)` + expectFailNowed(t, fakeT, expected) + }) + t.Run("different error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + err := fmt.Errorf("the actual error") + ErrorIs(fakeT, err, os.ErrNotExist) + expected := `assertion failed: error is "the actual error", not "file does not exist" (os.ErrNotExist)` + expectFailNowed(t, fakeT, expected) + }) + t.Run("same error", func(t *testing.T) { + fakeT := &fakeTestingT{} + + err := fmt.Errorf("some wrapping: %w", os.ErrNotExist) + ErrorIs(fakeT, err, os.ErrNotExist) + expectSuccess(t, fakeT) + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/call.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/call.go new file mode 100644 index 0000000..c93997b --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/call.go @@ -0,0 +1,201 @@ +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/token" +) + +// call wraps a testify/assert ast.CallExpr and exposes properties of the +// expression to facilitate migrating the expression to a gotest.tools/v3/assert +type call struct { + fileset *token.FileSet + expr *ast.CallExpr + xIdent *ast.Ident + selExpr *ast.SelectorExpr + // assert is Assert (if the testify package was require), or Check (if the + // testify package was assert). + assert string +} + +func (c call) String() string { + buf := new(bytes.Buffer) + // nolint: errcheck + format.Node(buf, token.NewFileSet(), c.expr) + return buf.String() +} + +func (c call) StringWithFileInfo() string { + if c.fileset.File(c.expr.Pos()) == nil { + return fmt.Sprintf("%s at unknown file", c) + } + return fmt.Sprintf("%s at %s:%d", c, + relativePath(c.fileset.File(c.expr.Pos()).Name()), + c.fileset.Position(c.expr.Pos()).Line) +} + +// testingT returns the first argument of the call, which is assumed to be a +// *ast.Ident of *testing.T or a compatible interface. +func (c call) testingT() ast.Expr { + if len(c.expr.Args) == 0 { + return nil + } + return c.expr.Args[0] +} + +// extraArgs returns the arguments of the expression starting from index +func (c call) extraArgs(index int) []ast.Expr { + if len(c.expr.Args) <= index { + return nil + } + return c.expr.Args[index:] +} + +// args returns a range of arguments from the expression +func (c call) args(from, to int) []ast.Expr { + return c.expr.Args[from:to] +} + +// arg returns a single argument from the expression +func (c call) arg(index int) ast.Expr { + return c.expr.Args[index] +} + +// newCallFromCallExpr returns a new call build from a ast.CallExpr. Returns false +// if the call expression does not have the expected ast nodes. +func newCallFromCallExpr(callExpr *ast.CallExpr, migration migration) (call, bool) { + c := call{} + selector, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return c, false + } + ident, ok := selector.X.(*ast.Ident) + if !ok { + return c, false + } + + return call{ + fileset: migration.fileset, + xIdent: ident, + selExpr: selector, + expr: callExpr, + }, true +} + +// newTestifyCallFromNode returns a call that wraps a valid testify assertion. +// Returns false if the call expression is not a testify assertion. +func newTestifyCallFromNode(callExpr *ast.CallExpr, migration migration) (call, bool) { + tcall, ok := newCallFromCallExpr(callExpr, migration) + if !ok { + return tcall, false + } + + testifyNewAssignStmt := testifyAssertionsAssignment(tcall, migration) + switch { + case testifyNewAssignStmt != nil: + return updateCallForTestifyNew(tcall, testifyNewAssignStmt, migration) + case isTestifyPkgCall(tcall, migration): + tcall.assert = migration.importNames.funcNameFromTestifyName(tcall.xIdent.Name) + return tcall, true + } + return tcall, false +} + +// isTestifyPkgCall returns true if the call is a testify package-level assertion +// (as apposed to an assertion method on the Assertions type) +// +// TODO: check if the xIdent.Obj.Decl is an import declaration instead of +// assuming that a name matching the import name is always an import. Some code +// may shadow import names, which could lead to incorrect results. +func isTestifyPkgCall(tcall call, migration migration) bool { + return migration.importNames.matchesTestify(tcall.xIdent) +} + +// testifyAssertionsAssignment returns an ast.AssignStmt if the call is a testify +// call from an Assertions object returned from assert.New(t) (not a package level +// assert). Otherwise returns nil. +func testifyAssertionsAssignment(tcall call, migration migration) *ast.AssignStmt { + if tcall.xIdent.Obj == nil { + return nil + } + + assignStmt, ok := tcall.xIdent.Obj.Decl.(*ast.AssignStmt) + if !ok { + return nil + } + + if isAssignmentFromAssertNew(assignStmt, migration) { + return assignStmt + } + return nil +} + +func updateCallForTestifyNew( + tcall call, + testifyNewAssignStmt *ast.AssignStmt, + migration migration, +) (call, bool) { + testifyNewCallExpr := callExprFromAssignment(testifyNewAssignStmt) + if testifyNewCallExpr == nil { + return tcall, false + } + testifyNewCall, ok := newCallFromCallExpr(testifyNewCallExpr, migration) + if !ok { + return tcall, false + } + + tcall.assert = migration.importNames.funcNameFromTestifyName(testifyNewCall.xIdent.Name) + tcall.expr = addMissingTestingTArgToCallExpr(tcall.expr, testifyNewCall.testingT()) + return tcall, true +} + +// addMissingTestingTArgToCallExpr adds a testingT arg as the first arg of the +// ast.CallExpr and returns a copy of the ast.CallExpr +func addMissingTestingTArgToCallExpr(callExpr *ast.CallExpr, testingT ast.Expr) *ast.CallExpr { + return &ast.CallExpr{ + Fun: callExpr.Fun, + Args: append([]ast.Expr{removePos(testingT)}, callExpr.Args...), + } +} + +func removePos(node ast.Expr) ast.Expr { + switch typed := node.(type) { + case *ast.Ident: + return &ast.Ident{Name: typed.Name} + } + return node +} + +// TODO: use pkgInfo and walkForType instead? +func isAssignmentFromAssertNew(assign *ast.AssignStmt, migration migration) bool { + callExpr := callExprFromAssignment(assign) + if callExpr == nil { + return false + } + tcall, ok := newCallFromCallExpr(callExpr, migration) + if !ok { + return false + } + if !migration.importNames.matchesTestify(tcall.xIdent) { + return false + } + + if len(tcall.expr.Args) != 1 { + return false + } + return tcall.selExpr.Sel.Name == "New" +} + +func callExprFromAssignment(assign *ast.AssignStmt) *ast.CallExpr { + if len(assign.Rhs) != 1 { + return nil + } + + callExpr, ok := assign.Rhs[0].(*ast.CallExpr) + if !ok { + return nil + } + return callExpr +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/call_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/call_test.go new file mode 100644 index 0000000..aaf7cc9 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/call_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "go/ast" + "go/token" + "testing" + + "gotest.tools/v3/assert" +) + +func TestCall_String(t *testing.T) { + c := &call{ + expr: &ast.CallExpr{Fun: ast.NewIdent("myFunc")}, + } + assert.Equal(t, c.String(), "myFunc()") +} + +func TestCall_StringWithFileInfo(t *testing.T) { + c := &call{ + fileset: token.NewFileSet(), + expr: &ast.CallExpr{ + Fun: &ast.Ident{ + Name: "myFunc", + NamePos: 17, + }}, + } + t.Run("unknown file", func(t *testing.T) { + assert.Equal(t, c.StringWithFileInfo(), "myFunc() at unknown file") + }) + + t.Run("at position", func(t *testing.T) { + c.fileset.AddFile("source.go", 10, 100) + assert.Equal(t, c.StringWithFileInfo(), "myFunc() at source.go:1") + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/doc.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/doc.go new file mode 100644 index 0000000..da953bf --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/doc.go @@ -0,0 +1,22 @@ +/* + +Command gty-migrate-from-testify migrates packages from +testify/assert and testify/require to gotest.tools/v3/assert. + + $ go get gotest.tools/v3/assert/cmd/gty-migrate-from-testify + +Usage: + + gty-migrate-from-testify [OPTIONS] PACKAGE [PACKAGE...] + +See --help for full usage. + + +To run on all packages (including external test packages) use: + + go list \ + -f '{{.ImportPath}} {{if .XTestGoFiles}}{{"\n"}}{{.ImportPath}}_test{{end}}' \ + ./... | xargs gty-migrate-from-testify + +*/ +package main diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/main.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/main.go new file mode 100644 index 0000000..e788654 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/main.go @@ -0,0 +1,262 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/format" + "go/token" + "io/ioutil" + "log" + "os" + "path" + "path/filepath" + "strings" + + "github.com/spf13/pflag" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/imports" +) + +type options struct { + pkgs []string + dryRun bool + debug bool + cmpImportName string + showLoaderErrors bool + buildFlags []string + localImportPath string +} + +func main() { + name := os.Args[0] + flags, opts := setupFlags(name) + handleExitError(name, flags.Parse(os.Args[1:])) + setupLogging(opts) + opts.pkgs = flags.Args() + handleExitError(name, run(*opts)) +} + +func setupLogging(opts *options) { + log.SetFlags(0) + enableDebug = opts.debug +} + +var enableDebug = false + +func debugf(msg string, args ...interface{}) { + if enableDebug { + log.Printf("DEBUG: "+msg, args...) + } +} + +func setupFlags(name string) (*pflag.FlagSet, *options) { + opts := options{} + flags := pflag.NewFlagSet(name, pflag.ContinueOnError) + flags.BoolVar(&opts.dryRun, "dry-run", false, + "don't write changes to file") + flags.BoolVar(&opts.debug, "debug", false, "enable debug logging") + flags.StringVar(&opts.cmpImportName, "cmp-pkg-import-alias", "is", + "import alias to use for the assert/cmp package") + flags.BoolVar(&opts.showLoaderErrors, "print-loader-errors", false, + "print errors from loading source") + flags.StringSliceVar(&opts.buildFlags, "build-tags", nil, + "build to pass to Go when loading source files") + flags.StringVar(&opts.localImportPath, "local-import-path", "", + "value to pass to 'goimports -local' flag for sorting local imports") + flags.Usage = func() { + fmt.Fprintf(os.Stderr, `Usage: %s [OPTIONS] PACKAGE [PACKAGE...] + +Migrate calls from testify/{assert|require} to gotest.tools/v3/assert. + +%s`, name, flags.FlagUsages()) + } + return flags, &opts +} + +func handleExitError(name string, err error) { + switch { + case err == nil: + return + case errors.Is(err, pflag.ErrHelp): + os.Exit(0) + default: + log.Println(name + ": Error: " + err.Error()) + os.Exit(3) + } +} + +func run(opts options) error { + imports.LocalPrefix = opts.localImportPath + + fset := token.NewFileSet() + pkgs, err := loadPackages(opts, fset) + if err != nil { + return fmt.Errorf("failed to load program: %w", err) + } + + debugf("package count: %d", len(pkgs)) + for _, pkg := range pkgs { + debugf("file count for package %v: %d", pkg.PkgPath, len(pkg.Syntax)) + for _, astFile := range pkg.Syntax { + absFilename := fset.File(astFile.Pos()).Name() + filename := relativePath(absFilename) + importNames := newImportNames(astFile.Imports, opts) + if !importNames.hasTestifyImports() { + debugf("skipping file %s, no imports", filename) + continue + } + + debugf("migrating %s with imports: %#v", filename, importNames) + m := migration{ + file: astFile, + fileset: fset, + importNames: importNames, + pkgInfo: pkg.TypesInfo, + } + migrateFile(m) + if opts.dryRun { + continue + } + + raw, err := formatFile(m) + if err != nil { + return fmt.Errorf("failed to format %s: %w", filename, err) + } + + if err := ioutil.WriteFile(absFilename, raw, 0); err != nil { + return fmt.Errorf("failed to write file %s: %w", filename, err) + } + } + } + + return nil +} + +var loadMode = packages.NeedName | + packages.NeedFiles | + packages.NeedCompiledGoFiles | + packages.NeedDeps | + packages.NeedImports | + packages.NeedTypes | + packages.NeedTypesInfo | + packages.NeedTypesSizes | + packages.NeedSyntax + +func loadPackages(opts options, fset *token.FileSet) ([]*packages.Package, error) { + conf := &packages.Config{ + Mode: loadMode, + Fset: fset, + Tests: true, + Logf: debugf, + BuildFlags: opts.buildFlags, + } + + pkgs, err := packages.Load(conf, opts.pkgs...) + if err != nil { + return nil, err + } + if opts.showLoaderErrors { + packages.PrintErrors(pkgs) + } + return pkgs, nil +} + +func relativePath(p string) string { + cwd, err := os.Getwd() + if err != nil { + return p + } + rel, err := filepath.Rel(cwd, p) + if err != nil { + return p + } + return rel +} + +type importNames struct { + testifyAssert string + testifyRequire string + assert string + cmp string +} + +func (p importNames) hasTestifyImports() bool { + return p.testifyAssert != "" || p.testifyRequire != "" +} + +func (p importNames) matchesTestify(ident *ast.Ident) bool { + return ident.Name == p.testifyAssert || ident.Name == p.testifyRequire +} + +func (p importNames) funcNameFromTestifyName(name string) string { + switch name { + case p.testifyAssert: + return funcNameCheck + case p.testifyRequire: + return funcNameAssert + default: + panic("unexpected testify import name " + name) + } +} + +func newImportNames(imports []*ast.ImportSpec, opt options) importNames { + defaultAssertAlias := path.Base(pkgAssert) + importNames := importNames{ + assert: defaultAssertAlias, + cmp: path.Base(pkgCmp), + } + for _, spec := range imports { + switch strings.Trim(spec.Path.Value, `"`) { + case pkgTestifyAssert, pkgGopkgTestifyAssert: + importNames.testifyAssert = identOrDefault(spec.Name, "assert") + case pkgTestifyRequire, pkgGopkgTestifyRequire: + importNames.testifyRequire = identOrDefault(spec.Name, "require") + default: + pkgPath := strings.Trim(spec.Path.Value, `"`) + + switch { + // v3/assert is already imported and has an alias + case pkgPath == pkgAssert: + if spec.Name != nil && spec.Name.Name != "" { + importNames.assert = spec.Name.Name + } + continue + + // some other package is imported as assert + case importedAs(spec, path.Base(pkgAssert)) && importNames.assert == defaultAssertAlias: + importNames.assert = "gtyassert" + } + } + } + + if opt.cmpImportName != "" { + importNames.cmp = opt.cmpImportName + } + return importNames +} + +func importedAs(spec *ast.ImportSpec, pkg string) bool { + if path.Base(strings.Trim(spec.Path.Value, `"`)) == pkg { + return true + } + return spec.Name != nil && spec.Name.Name == pkg +} + +func identOrDefault(ident *ast.Ident, def string) string { + if ident != nil { + return ident.Name + } + return def +} + +func formatFile(migration migration) ([]byte, error) { + buf := new(bytes.Buffer) + err := format.Node(buf, migration.fileset, migration.file) + if err != nil { + return nil, err + } + filename := migration.fileset.File(migration.file.Pos()).Name() + return imports.Process(filename, buf.Bytes(), nil) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/main_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/main_test.go new file mode 100644 index 0000000..e5accbb --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/main_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "io/ioutil" + "testing" + + "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert" + "gotest.tools/v3/env" + "gotest.tools/v3/fs" + "gotest.tools/v3/golden" +) + +func TestRun(t *testing.T) { + setupLogging(&options{}) + dir := fs.NewDir(t, "test-run", + fs.WithDir("src/example.com/example", fs.FromDir("testdata/full"))) + defer dir.Remove() + + defer env.Patch(t, "GO111MODULE", "off")() + defer env.Patch(t, "GOPATH", dir.Path())() + err := run(options{ + pkgs: []string{"example.com/example"}, + showLoaderErrors: true, + }) + assert.NilError(t, err) + + raw, err := ioutil.ReadFile(dir.Join("src/example.com/example/some_test.go")) + assert.NilError(t, err) + golden.Assert(t, string(raw), "full-expected/some_test.go") +} + +func TestSetupFlags(t *testing.T) { + flags, opts := setupFlags("testing") + assert.Assert(t, flags.Usage != nil) + + err := flags.Parse([]string{ + "--dry-run", + "--debug", + "--cmp-pkg-import-alias=foo", + "--print-loader-errors", + }) + assert.NilError(t, err) + expected := &options{ + dryRun: true, + debug: true, + cmpImportName: "foo", + showLoaderErrors: true, + } + assert.DeepEqual(t, opts, expected, cmpOptions) +} + +var cmpOptions = cmp.AllowUnexported(options{}) diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/migrate.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/migrate.go new file mode 100644 index 0000000..42dbe5e --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/migrate.go @@ -0,0 +1,389 @@ +package main + +import ( + "go/ast" + "go/token" + "go/types" + "log" + "path" + + "golang.org/x/tools/go/ast/astutil" +) + +const ( + pkgTestifyAssert = "github.com/stretchr/testify/assert" + pkgGopkgTestifyAssert = "gopkg.in/stretchr/testify.v1/assert" + pkgTestifyRequire = "github.com/stretchr/testify/require" + pkgGopkgTestifyRequire = "gopkg.in/stretchr/testify.v1/require" + pkgAssert = "gotest.tools/v3/assert" + pkgCmp = "gotest.tools/v3/assert/cmp" +) + +const ( + funcNameAssert = "Assert" + funcNameCheck = "Check" +) + +var allTestifyPks = []string{ + pkgTestifyAssert, + pkgTestifyRequire, + pkgGopkgTestifyAssert, + pkgGopkgTestifyRequire, +} + +type migration struct { + file *ast.File + fileset *token.FileSet + importNames importNames + pkgInfo *types.Info +} + +func migrateFile(migration migration) { + astutil.Apply(migration.file, nil, replaceCalls(migration)) + updateImports(migration) +} + +func updateImports(migration migration) { + for _, remove := range allTestifyPks { + astutil.DeleteImport(migration.fileset, migration.file, remove) + } + + var alias string + if migration.importNames.assert != path.Base(pkgAssert) { + alias = migration.importNames.assert + } + astutil.AddNamedImport(migration.fileset, migration.file, alias, pkgAssert) + + if migration.importNames.cmp != path.Base(pkgCmp) { + alias = migration.importNames.cmp + } + astutil.AddNamedImport(migration.fileset, migration.file, alias, pkgCmp) +} + +type emptyNode struct{} + +func (n emptyNode) Pos() token.Pos { + return 0 +} + +func (n emptyNode) End() token.Pos { + return 0 +} + +var removeNode = emptyNode{} + +func replaceCalls(migration migration) func(cursor *astutil.Cursor) bool { + return func(cursor *astutil.Cursor) bool { + var newNode ast.Node + switch typed := cursor.Node().(type) { + case *ast.SelectorExpr: + newNode = getReplacementTestingT(typed, migration.importNames) + case *ast.CallExpr: + newNode = getReplacementAssertion(typed, migration) + case *ast.AssignStmt: + newNode = getReplacementAssignment(typed, migration) + } + + switch newNode { + case nil: + case removeNode: + cursor.Delete() + default: + cursor.Replace(newNode) + } + return true + } +} + +func getReplacementTestingT(selector *ast.SelectorExpr, names importNames) ast.Node { + xIdent, ok := selector.X.(*ast.Ident) + if !ok { + return nil + } + if selector.Sel.Name != "TestingT" || !names.matchesTestify(xIdent) { + return nil + } + return &ast.SelectorExpr{ + X: &ast.Ident{Name: names.assert, NamePos: xIdent.NamePos}, + Sel: selector.Sel, + } +} + +func getReplacementAssertion(callExpr *ast.CallExpr, migration migration) ast.Node { + tcall, ok := newTestifyCallFromNode(callExpr, migration) + if !ok { + return nil + } + if len(tcall.expr.Args) < 2 { + return convertTestifySingleArgCall(tcall) + } + return convertTestifyAssertion(tcall, migration) +} + +func getReplacementAssignment(assign *ast.AssignStmt, migration migration) ast.Node { + if isAssignmentFromAssertNew(assign, migration) { + return removeNode + } + return nil +} + +func convertTestifySingleArgCall(tcall call) ast.Node { + switch tcall.selExpr.Sel.Name { + case "TestingT": + // handled as SelectorExpr + return nil + case "New": + // handled by getReplacementAssignment + return nil + default: + log.Printf("%s: skipping unknown selector", tcall.StringWithFileInfo()) + return nil + } +} + +// nolint: maintidx +func convertTestifyAssertion(tcall call, migration migration) ast.Node { + imports := migration.importNames + + switch tcall.selExpr.Sel.Name { + case "NoError", "NoErrorf": + return convertNoError(tcall, imports) + case "True", "Truef": + return convertTrue(tcall, imports) + case "False", "Falsef": + return convertFalse(tcall, imports) + case "Equal", "Equalf", "Exactly", "Exactlyf", "EqualValues", "EqualValuesf": + return convertEqual(tcall, migration) + case "Contains", "Containsf": + return convertTwoArgComparison(tcall, imports, "Contains") + case "Len", "Lenf": + return convertTwoArgComparison(tcall, imports, "Len") + case "Panics", "Panicsf": + return convertOneArgComparison(tcall, imports, "Panics") + case "EqualError", "EqualErrorf": + return convertEqualError(tcall, imports) + case "Error", "Errorf": + return convertError(tcall, imports) + case "ErrorContains", "ErrorContainsf": + return convertErrorContains(tcall, imports) + case "Empty", "Emptyf": + return convertEmpty(tcall, imports) + case "Nil", "Nilf": + return convertNil(tcall, migration) + case "NotNil", "NotNilf": + return convertNegativeComparison(tcall, imports, &ast.Ident{Name: "nil"}, 2) + case "NotEqual", "NotEqualf": + return convertNegativeComparison(tcall, imports, tcall.arg(2), 3) + case "Fail", "Failf": + return convertFail(tcall, "Error") + case "FailNow", "FailNowf": + return convertFail(tcall, "Fatal") + case "NotEmpty", "NotEmptyf": + return convertNotEmpty(tcall, imports) + case "NotZero", "NotZerof": + zero := &ast.BasicLit{Kind: token.INT, Value: "0"} + return convertNegativeComparison(tcall, imports, zero, 2) + } + log.Printf("%s: skipping unsupported assertion", tcall.StringWithFileInfo()) + return nil +} + +func newCallExpr(x, sel string, args []ast.Expr) *ast.CallExpr { + return &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: x}, + Sel: &ast.Ident{Name: sel}, + }, + Args: args, + } +} + +func newCallExprArgs(t ast.Expr, cmp ast.Expr, extra ...ast.Expr) []ast.Expr { + return append(append([]ast.Expr{t}, cmp), extra...) +} + +func newCallExprWithPosition(tcall call, imports importNames, args []ast.Expr) *ast.CallExpr { + return &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: imports.assert, + NamePos: tcall.xIdent.NamePos, + }, + Sel: &ast.Ident{Name: tcall.assert}, + }, + Args: args, + } +} + +func convertNoError(tcall call, imports importNames) ast.Node { + // use assert.NilError() for require.NoError() + if tcall.assert == funcNameAssert { + return newCallExprWithoutComparison(tcall, imports, "NilError") + } + // use assert.Check() for assert.NoError() + return newCallExprWithoutComparison(tcall, imports, "Check") +} + +func convertEqualError(tcall call, imports importNames) ast.Node { + if tcall.assert == funcNameAssert { + return newCallExprWithoutComparison(tcall, imports, "Error") + } + return convertTwoArgComparison(tcall, imports, "Error") +} + +func newCallExprWithoutComparison(tcall call, imports importNames, name string) ast.Node { + return &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: imports.assert, + NamePos: tcall.xIdent.NamePos, + }, + Sel: &ast.Ident{Name: name}, + }, + Args: tcall.expr.Args, + } +} + +func convertOneArgComparison(tcall call, imports importNames, cmpName string) ast.Node { + return newCallExprWithPosition(tcall, imports, + newCallExprArgs( + tcall.testingT(), + newCallExpr(imports.cmp, cmpName, []ast.Expr{tcall.arg(1)}), + tcall.extraArgs(2)...)) +} + +func convertTrue(tcall call, imports importNames) ast.Node { + return newCallExprWithPosition(tcall, imports, tcall.expr.Args) +} + +func convertFalse(tcall call, imports importNames) ast.Node { + return newCallExprWithPosition(tcall, imports, + newCallExprArgs( + tcall.testingT(), + &ast.UnaryExpr{Op: token.NOT, X: tcall.arg(1)}, + tcall.extraArgs(2)...)) +} + +func convertEqual(tcall call, migration migration) ast.Node { + imports := migration.importNames + + hasExtraArgs := len(tcall.extraArgs(3)) > 0 + + cmpEqual := convertTwoArgComparison(tcall, imports, "Equal") + if tcall.assert == funcNameAssert { + cmpEqual = newCallExprWithoutComparison(tcall, imports, "Equal") + } + cmpDeepEqual := convertTwoArgComparison(tcall, imports, "DeepEqual") + if tcall.assert == funcNameAssert && !hasExtraArgs { + cmpDeepEqual = newCallExprWithoutComparison(tcall, imports, "DeepEqual") + } + + gotype := walkForType(migration.pkgInfo, tcall.arg(1)) + if isUnknownType(gotype) { + gotype = walkForType(migration.pkgInfo, tcall.arg(2)) + } + if isUnknownType(gotype) { + return cmpDeepEqual + } + + switch gotype.Underlying().(type) { + case *types.Basic: + return cmpEqual + default: + return cmpDeepEqual + } +} + +func convertTwoArgComparison(tcall call, imports importNames, cmpName string) ast.Node { + return newCallExprWithPosition(tcall, imports, + newCallExprArgs( + tcall.testingT(), + newCallExpr(imports.cmp, cmpName, tcall.args(1, 3)), + tcall.extraArgs(3)...)) +} + +func convertError(tcall call, imports importNames) ast.Node { + cmpArgs := []ast.Expr{ + tcall.arg(1), + &ast.BasicLit{Kind: token.STRING, Value: `""`}} + + return newCallExprWithPosition(tcall, imports, + newCallExprArgs( + tcall.testingT(), + newCallExpr(imports.cmp, "ErrorContains", cmpArgs), + tcall.extraArgs(2)...)) +} + +func convertErrorContains(tcall call, imports importNames) ast.Node { + return &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: imports.assert, + NamePos: tcall.xIdent.NamePos, + }, + Sel: &ast.Ident{Name: "ErrorContains"}, + }, + Args: tcall.expr.Args, + } +} + +func convertEmpty(tcall call, imports importNames) ast.Node { + cmpArgs := []ast.Expr{ + tcall.arg(1), + &ast.BasicLit{Kind: token.INT, Value: "0"}, + } + return newCallExprWithPosition(tcall, imports, + newCallExprArgs( + tcall.testingT(), + newCallExpr(imports.cmp, "Len", cmpArgs), + tcall.extraArgs(2)...)) +} + +func convertNil(tcall call, migration migration) ast.Node { + gotype := walkForType(migration.pkgInfo, tcall.arg(1)) + if gotype != nil && gotype.String() == "error" { + return convertNoError(tcall, migration.importNames) + } + return convertOneArgComparison(tcall, migration.importNames, "Nil") +} + +func convertNegativeComparison( + tcall call, + imports importNames, + right ast.Expr, + extra int, +) ast.Node { + return newCallExprWithPosition(tcall, imports, + newCallExprArgs( + tcall.testingT(), + &ast.BinaryExpr{X: tcall.arg(1), Op: token.NEQ, Y: right}, + tcall.extraArgs(extra)...)) +} + +func convertFail(tcall call, selector string) ast.Node { + extraArgs := tcall.extraArgs(1) + if len(extraArgs) > 1 { + selector += "f" + } + + return &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: tcall.testingT(), + Sel: &ast.Ident{Name: selector}, + }, + Args: extraArgs, + } +} + +func convertNotEmpty(tcall call, imports importNames) ast.Node { + lenExpr := &ast.CallExpr{ + Fun: &ast.Ident{Name: "len"}, + Args: tcall.args(1, 2), + } + zeroExpr := &ast.BasicLit{Kind: token.INT, Value: "0"} + return newCallExprWithPosition(tcall, imports, + newCallExprArgs( + tcall.testingT(), + &ast.BinaryExpr{X: lenExpr, Op: token.NEQ, Y: zeroExpr}, + tcall.extraArgs(2)...)) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/migrate_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/migrate_test.go new file mode 100644 index 0000000..a3ec56e --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/migrate_test.go @@ -0,0 +1,381 @@ +package main + +import ( + "go/token" + "testing" + + "golang.org/x/tools/go/packages" + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/env" + "gotest.tools/v3/fs" + "gotest.tools/v3/icmd" +) + +func TestMigrateFileReplacesTestingT(t *testing.T) { + source := ` +package foo + +import ( + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSomething(t *testing.T) { + a := assert.TestingT(t) + b := require.TestingT(t) + c := require.TestingT(t) + if a == b {} + _ = c +} + +func do(t require.TestingT) {} +` + migration := newMigrationFromSource(t, source) + migrateFile(migration) + + expected := `package foo + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestSomething(t *testing.T) { + a := assert.TestingT(t) + b := assert.TestingT(t) + c := assert.TestingT(t) + if a == b { + } + _ = c +} + +func do(t assert.TestingT) {} +` + actual, err := formatFile(migration) + assert.NilError(t, err) + assert.Assert(t, cmp.Equal(expected, string(actual))) +} + +func newMigrationFromSource(t *testing.T, source string) migration { + t.Helper() + goMod := `module example.com/foo + +require github.com/stretchr/testify v1.7.1 +` + + dir := fs.NewDir(t, t.Name(), + fs.WithFile("foo.go", source), + fs.WithFile("go.mod", goMod)) + fileset := token.NewFileSet() + + env.ChangeWorkingDir(t, dir.Path()) + icmd.RunCommand("go", "mod", "tidy").Assert(t, icmd.Success) + + opts := options{pkgs: []string{"./..."}} + pkgs, err := loadPackages(opts, fileset) + assert.NilError(t, err) + packages.PrintErrors(pkgs) + + pkg := pkgs[0] + assert.Assert(t, !pkg.IllTyped) + + return migration{ + file: pkg.Syntax[0], + fileset: fileset, + importNames: newImportNames(pkg.Syntax[0].Imports, opts), + pkgInfo: pkg.TypesInfo, + } +} + +func TestMigrateFileWithNamedCmpPackage(t *testing.T) { + source := ` +package foo + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + assert.Equal(t, "a", "b") +} +` + migration := newMigrationFromSource(t, source) + migration.importNames.cmp = "is" + migrateFile(migration) + + expected := `package foo + +import ( + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestSomething(t *testing.T) { + assert.Check(t, is.Equal("a", "b")) +} +` + actual, err := formatFile(migration) + assert.NilError(t, err) + assert.Assert(t, cmp.Equal(expected, string(actual))) +} + +func TestMigrateFileWithCommentsOnAssert(t *testing.T) { + source := ` +package foo + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + // This is going to fail + assert.Equal(t, "a", "b") +} +` + migration := newMigrationFromSource(t, source) + migrateFile(migration) + + expected := `package foo + +import ( + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +func TestSomething(t *testing.T) { + // This is going to fail + assert.Check(t, cmp.Equal("a", "b")) +} +` + actual, err := formatFile(migration) + assert.NilError(t, err) + assert.Assert(t, cmp.Equal(expected, string(actual))) +} + +func TestMigrateFileConvertNilToNilError(t *testing.T) { + source := ` +package foo + +import ( + "testing" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + var err error + assert.Nil(t, err) + require.Nil(t, err) +} +` + migration := newMigrationFromSource(t, source) + migrateFile(migration) + + expected := `package foo + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestSomething(t *testing.T) { + var err error + assert.Check(t, err) + assert.NilError(t, err) +} +` + actual, err := formatFile(migration) + assert.NilError(t, err) + assert.Assert(t, cmp.Equal(expected, string(actual))) +} + +func TestMigrateFileConvertAssertNew(t *testing.T) { + source := ` +package foo + +import ( + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSomething(t *testing.T) { + is := assert.New(t) + is.Equal("one", "two") + is.NotEqual("one", "two") + + assert := require.New(t) + assert.Equal("one", "two") + assert.NotEqual("one", "two") +} + +func TestOtherName(z *testing.T) { + is := require.New(z) + is.Equal("one", "two") +} + +` + migration := newMigrationFromSource(t, source) + migrateFile(migration) + + expected := `package foo + +import ( + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +func TestSomething(t *testing.T) { + + assert.Check(t, cmp.Equal("one", "two")) + assert.Check(t, "one" != "two") + + assert.Equal(t, "one", "two") + assert.Assert(t, "one" != "two") +} + +func TestOtherName(z *testing.T) { + + assert.Equal(z, "one", "two") +} +` + actual, err := formatFile(migration) + assert.NilError(t, err) + assert.Assert(t, cmp.Equal(expected, string(actual))) +} + +func TestMigrateFileWithExtraArgs(t *testing.T) { + source := ` +package foo + +import ( + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSomething(t *testing.T) { + var err error + assert.Error(t, err, "this is a comment") + require.ErrorContains(t, err, "this in the error") + assert.Empty(t, nil, "more comment") + require.Equal(t, []string{}, []string{}, "because") +} +` + migration := newMigrationFromSource(t, source) + migration.importNames.cmp = "is" + migrateFile(migration) + + expected := `package foo + +import ( + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestSomething(t *testing.T) { + var err error + assert.Check(t, is.ErrorContains(err, ""), "this is a comment") + assert.ErrorContains(t, err, "this in the error") + assert.Check(t, is.Len(nil, 0), "more comment") + assert.Assert(t, is.DeepEqual([]string{}, []string{}), "because") +} +` + actual, err := formatFile(migration) + assert.NilError(t, err) + assert.Assert(t, cmp.Equal(expected, string(actual))) +} + +func TestMigrate_AssertAlreadyImported(t *testing.T) { + source := ` +package foo + +import ( + "testing" + "github.com/stretchr/testify/require" + "gotest.tools/v3/assert" +) + +func TestSomething(t *testing.T) { + var err error + assert.Error(t, err, "this is the error") + require.Equal(t, []string{}, []string{}, "because") +} +` + migration := newMigrationFromSource(t, source) + migration.importNames.cmp = "is" + migrateFile(migration) + + expected := `package foo + +import ( + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestSomething(t *testing.T) { + var err error + assert.Error(t, err, "this is the error") + assert.Assert(t, is.DeepEqual([]string{}, []string{}), "because") +} +` + actual, err := formatFile(migration) + assert.NilError(t, err) + assert.Assert(t, cmp.Equal(expected, string(actual))) +} + +func TestMigrate_AssertAlreadyImportedWithAlias(t *testing.T) { + source := ` +package foo + +import ( + "testing" + "github.com/stretchr/testify/require" + gtya "gotest.tools/v3/assert" +) + +func TestSomething(t *testing.T) { + var err error + gtya.Error(t, err, "this is the error") + require.Equal(t, []string{}, []string{}, "because") +} +` + migration := newMigrationFromSource(t, source) + migration.importNames.cmp = "is" + migrateFile(migration) + + expected := `package foo + +import ( + "testing" + + gtya "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestSomething(t *testing.T) { + var err error + gtya.Error(t, err, "this is the error") + gtya.Assert(t, is.DeepEqual([]string{}, []string{}), "because") +} +` + actual, err := formatFile(migration) + assert.NilError(t, err) + assert.Assert(t, cmp.Equal(expected, string(actual))) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/testdata/full-expected/some_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/testdata/full-expected/some_test.go new file mode 100644 index 0000000..1cbdf0f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/testdata/full-expected/some_test.go @@ -0,0 +1,150 @@ +package foo + +import ( + "fmt" + "testing" + + "github.com/go-check/check" + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +type mystruct struct { + a int + expected int +} + +func TestFirstThing(t *testing.T) { + rt := assert.TestingT(t) + assert.Check(t, cmp.Equal("foo", "bar")) + assert.Check(t, cmp.Equal(1, 2)) + assert.Check(t, false) + assert.Check(t, !true) + assert.NilError(rt, nil) + + assert.Check(t, cmp.DeepEqual(map[string]bool{"a": true}, nil)) + assert.Check(t, cmp.DeepEqual([]int{1}, nil)) + assert.Equal(rt, "a", "B") +} + +func TestSecondThing(t *testing.T) { + var foo mystruct + assert.DeepEqual(t, foo, mystruct{}) + + assert.DeepEqual(t, mystruct{}, mystruct{}) + + assert.Check(t, nil, "foo %d", 3) + assert.NilError(t, nil, "foo %d", 3) + + assert.Check(t, cmp.ErrorContains(fmt.Errorf("foo"), "")) + + assert.Assert(t, 77 != 0) +} + +func TestOthers(t *testing.T) { + assert.Check(t, cmp.Contains([]string{}, "foo")) + assert.Assert(t, cmp.Len([]int{}, 3)) + assert.Check(t, cmp.Panics(func() { panic("foo") })) + assert.Error(t, fmt.Errorf("bad days"), "good days") + assert.Check(t, nil != nil) + + t.Error("why") + t.Fatal("why not") + assert.Assert(t, len([]bool{}) != 0) + + // Unsupported asseert + assert.NotContains(t, []bool{}, true) +} + +func TestAssertNew(t *testing.T) { + + assert.Check(t, cmp.Equal("a", "b")) +} + +type unit struct { + c *testing.T +} + +func thing(t *testing.T) unit { + return unit{c: t} +} + +func TestStoredTestingT(t *testing.T) { + u := thing(t) + assert.Check(u.c, cmp.Equal("A", "b")) + + u = unit{c: t} + assert.Check(u.c, cmp.Equal("A", "b")) +} + +func TestNotNamedT(c *testing.T) { + assert.Check(c, cmp.Equal("A", "b")) +} + +func TestEqualsWithComplexTypes(t *testing.T) { + expected := []int{1, 2, 3} + assert.Check(t, cmp.DeepEqual(expected, nil)) + + expectedM := map[int]bool{} + assert.Check(t, cmp.DeepEqual(expectedM, nil)) + + expectedI := 123 + assert.Check(t, cmp.Equal(expectedI, 0)) + + assert.Check(t, cmp.Equal(doInt(), 3)) + // TODO: struct field +} + +func doInt() int { + return 1 +} + +func TestEqualWithPrimitiveTypes(t *testing.T) { + s := "foo" + ptrString := &s + assert.Check(t, cmp.Equal(*ptrString, "foo")) + + assert.Check(t, cmp.Equal(doInt(), doInt())) + + x := doInt() + y := doInt() + assert.Check(t, cmp.Equal(x, y)) + + tc := mystruct{a: 3, expected: 5} + assert.Check(t, cmp.Equal(tc.a, tc.expected)) +} + +func TestTableTest(t *testing.T) { + var testcases = []struct { + opts []string + actual string + expected string + expectedOpts []string + }{ + { + opts: []string{"a", "b"}, + actual: "foo", + expected: "else", + }, + } + + for _, testcase := range testcases { + assert.Check(t, cmp.Equal(testcase.actual, testcase.expected)) + assert.Check(t, cmp.DeepEqual(testcase.opts, testcase.expectedOpts)) + } +} + +func TestWithChecker(c *check.C) { + var err error + assert.Check(c, err) +} + +func HelperWithAssertTestingT(t assert.TestingT) { + var err error + assert.Check(t, err, "with assert.TestingT") +} + +func BenchmarkSomething(b *testing.B) { + var err error + assert.Check(b, err) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/testdata/full/some_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/testdata/full/some_test.go new file mode 100644 index 0000000..f510038 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/testdata/full/some_test.go @@ -0,0 +1,151 @@ +package foo + +import ( + "fmt" + "testing" + + "github.com/go-check/check" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mystruct struct { + a int + expected int +} + +func TestFirstThing(t *testing.T) { + rt := require.TestingT(t) + assert.Equal(t, "foo", "bar") + assert.Equal(t, 1, 2) + assert.True(t, false) + assert.False(t, true) + require.NoError(rt, nil) + + assert.Equal(t, map[string]bool{"a": true}, nil) + assert.Equal(t, []int{1}, nil) + require.Equal(rt, "a", "B") +} + +func TestSecondThing(t *testing.T) { + var foo mystruct + require.Equal(t, foo, mystruct{}) + + require.Equal(t, mystruct{}, mystruct{}) + + assert.NoError(t, nil, "foo %d", 3) + require.NoError(t, nil, "foo %d", 3) + + assert.Error(t, fmt.Errorf("foo")) + + require.NotZero(t, 77) +} + +func TestOthers(t *testing.T) { + assert.Contains(t, []string{}, "foo") + require.Len(t, []int{}, 3) + assert.Panics(t, func() { panic("foo") }) + require.EqualError(t, fmt.Errorf("bad days"), "good days") + assert.NotNil(t, nil) + + assert.Fail(t, "why") + assert.FailNow(t, "why not") + require.NotEmpty(t, []bool{}) + + // Unsupported asseert + assert.NotContains(t, []bool{}, true) +} + +func TestAssertNew(t *testing.T) { + a := assert.New(t) + + a.Equal("a", "b") +} + +type unit struct { + c *testing.T +} + +func thing(t *testing.T) unit { + return unit{c: t} +} + +func TestStoredTestingT(t *testing.T) { + u := thing(t) + assert.Equal(u.c, "A", "b") + + u = unit{c: t} + assert.Equal(u.c, "A", "b") +} + +func TestNotNamedT(c *testing.T) { + assert.Equal(c, "A", "b") +} + +func TestEqualsWithComplexTypes(t *testing.T) { + expected := []int{1, 2, 3} + assert.Equal(t, expected, nil) + + expectedM := map[int]bool{} + assert.Equal(t, expectedM, nil) + + expectedI := 123 + assert.Equal(t, expectedI, 0) + + assert.Equal(t, doInt(), 3) + // TODO: struct field +} + +func doInt() int { + return 1 +} + +func TestEqualWithPrimitiveTypes(t *testing.T) { + s := "foo" + ptrString := &s + assert.Equal(t, *ptrString, "foo") + + assert.Equal(t, doInt(), doInt()) + + x := doInt() + y := doInt() + assert.Equal(t, x, y) + + tc := mystruct{a: 3, expected: 5} + assert.Equal(t, tc.a, tc.expected) +} + +func TestTableTest(t *testing.T) { + var testcases = []struct { + opts []string + actual string + expected string + expectedOpts []string + }{ + { + opts: []string{"a", "b"}, + actual: "foo", + expected: "else", + }, + } + + for _, testcase := range testcases { + assert.Equal(t, testcase.actual, testcase.expected) + assert.Equal(t, testcase.opts, testcase.expectedOpts) + } +} + +func TestWithChecker(c *check.C) { + var err error + assert.NoError(c, err) +} + +func HelperWithAssertTestingT(t assert.TestingT) { + var err error + assert.NoError(t, err, "with assert.TestingT") +} + +func BenchmarkSomething(b *testing.B) { + var err error + assert.NoError(b, err) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/walktype.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/walktype.go new file mode 100644 index 0000000..423f0ca --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmd/gty-migrate-from-testify/walktype.go @@ -0,0 +1,31 @@ +package main + +import ( + "go/ast" + "go/types" +) + +// walkForType walks the AST tree and returns the type of the expression +func walkForType(pkgInfo *types.Info, node ast.Node) types.Type { + var result types.Type + + visit := func(node ast.Node) bool { + if expr, ok := node.(ast.Expr); ok { + if typeAndValue, ok := pkgInfo.Types[expr]; ok { + result = typeAndValue.Type + return false + } + } + return true + } + ast.Inspect(node, visit) + return result +} + +func isUnknownType(typ types.Type) bool { + if typ == nil { + return true + } + basic, ok := typ.(*types.Basic) + return ok && basic.Kind() == types.Invalid +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/compare.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/compare.go new file mode 100644 index 0000000..78f76e4 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/compare.go @@ -0,0 +1,394 @@ +/*Package cmp provides Comparisons for Assert and Check*/ +package cmp // import "gotest.tools/v3/assert/cmp" + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/google/go-cmp/cmp" + "gotest.tools/v3/internal/format" +) + +// Comparison is a function which compares values and returns ResultSuccess if +// the actual value matches the expected value. If the values do not match the +// Result will contain a message about why it failed. +type Comparison func() Result + +// DeepEqual compares two values using google/go-cmp +// (https://godoc.org/github.com/google/go-cmp/cmp) +// and succeeds if the values are equal. +// +// The comparison can be customized using comparison Options. +// Package http://pkg.go.dev/gotest.tools/v3/assert/opt provides some additional +// commonly used Options. +func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison { + return func() (result Result) { + defer func() { + if panicmsg, handled := handleCmpPanic(recover()); handled { + result = ResultFailure(panicmsg) + } + }() + diff := cmp.Diff(x, y, opts...) + if diff == "" { + return ResultSuccess + } + return multiLineDiffResult(diff, x, y) + } +} + +func handleCmpPanic(r interface{}) (string, bool) { + if r == nil { + return "", false + } + panicmsg, ok := r.(string) + if !ok { + panic(r) + } + switch { + case strings.HasPrefix(panicmsg, "cannot handle unexported field"): + return panicmsg, true + } + panic(r) +} + +func toResult(success bool, msg string) Result { + if success { + return ResultSuccess + } + return ResultFailure(msg) +} + +// RegexOrPattern may be either a *regexp.Regexp or a string that is a valid +// regexp pattern. +type RegexOrPattern interface{} + +// Regexp succeeds if value v matches regular expression re. +// +// Example: +// assert.Assert(t, cmp.Regexp("^[0-9a-f]{32}$", str)) +// r := regexp.MustCompile("^[0-9a-f]{32}$") +// assert.Assert(t, cmp.Regexp(r, str)) +func Regexp(re RegexOrPattern, v string) Comparison { + match := func(re *regexp.Regexp) Result { + return toResult( + re.MatchString(v), + fmt.Sprintf("value %q does not match regexp %q", v, re.String())) + } + + return func() Result { + switch regex := re.(type) { + case *regexp.Regexp: + return match(regex) + case string: + re, err := regexp.Compile(regex) + if err != nil { + return ResultFailure(err.Error()) + } + return match(re) + default: + return ResultFailure(fmt.Sprintf("invalid type %T for regex pattern", regex)) + } + } +} + +// Equal succeeds if x == y. See assert.Equal for full documentation. +func Equal(x, y interface{}) Comparison { + return func() Result { + switch { + case x == y: + return ResultSuccess + case isMultiLineStringCompare(x, y): + diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)}) + return multiLineDiffResult(diff, x, y) + } + return ResultFailureTemplate(` + {{- printf "%v" .Data.x}} ( + {{- with callArg 0 }}{{ formatNode . }} {{end -}} + {{- printf "%T" .Data.x -}} + ) != {{ printf "%v" .Data.y}} ( + {{- with callArg 1 }}{{ formatNode . }} {{end -}} + {{- printf "%T" .Data.y -}} + )`, + map[string]interface{}{"x": x, "y": y}) + } +} + +func isMultiLineStringCompare(x, y interface{}) bool { + strX, ok := x.(string) + if !ok { + return false + } + strY, ok := y.(string) + if !ok { + return false + } + return strings.Contains(strX, "\n") || strings.Contains(strY, "\n") +} + +func multiLineDiffResult(diff string, x, y interface{}) Result { + return ResultFailureTemplate(` +--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}} ++++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}} +{{ .Data.diff }}`, + map[string]interface{}{"diff": diff, "x": x, "y": y}) +} + +// Len succeeds if the sequence has the expected length. +func Len(seq interface{}, expected int) Comparison { + return func() (result Result) { + defer func() { + if e := recover(); e != nil { + result = ResultFailure(fmt.Sprintf("type %T does not have a length", seq)) + } + }() + value := reflect.ValueOf(seq) + length := value.Len() + if length == expected { + return ResultSuccess + } + msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected) + return ResultFailure(msg) + } +} + +// Contains succeeds if item is in collection. Collection may be a string, map, +// slice, or array. +// +// If collection is a string, item must also be a string, and is compared using +// strings.Contains(). +// If collection is a Map, contains will succeed if item is a key in the map. +// If collection is a slice or array, item is compared to each item in the +// sequence using reflect.DeepEqual(). +func Contains(collection interface{}, item interface{}) Comparison { + return func() Result { + colValue := reflect.ValueOf(collection) + if !colValue.IsValid() { + return ResultFailure("nil does not contain items") + } + msg := fmt.Sprintf("%v does not contain %v", collection, item) + + itemValue := reflect.ValueOf(item) + switch colValue.Type().Kind() { + case reflect.String: + if itemValue.Type().Kind() != reflect.String { + return ResultFailure("string may only contain strings") + } + return toResult( + strings.Contains(colValue.String(), itemValue.String()), + fmt.Sprintf("string %q does not contain %q", collection, item)) + + case reflect.Map: + if itemValue.Type() != colValue.Type().Key() { + return ResultFailure(fmt.Sprintf( + "%v can not contain a %v key", colValue.Type(), itemValue.Type())) + } + return toResult(colValue.MapIndex(itemValue).IsValid(), msg) + + case reflect.Slice, reflect.Array: + for i := 0; i < colValue.Len(); i++ { + if reflect.DeepEqual(colValue.Index(i).Interface(), item) { + return ResultSuccess + } + } + return ResultFailure(msg) + default: + return ResultFailure(fmt.Sprintf("type %T does not contain items", collection)) + } + } +} + +// Panics succeeds if f() panics. +func Panics(f func()) Comparison { + return func() (result Result) { + defer func() { + if err := recover(); err != nil { + result = ResultSuccess + } + }() + f() + return ResultFailure("did not panic") + } +} + +// Error succeeds if err is a non-nil error, and the error message equals the +// expected message. +func Error(err error, message string) Comparison { + return func() Result { + switch { + case err == nil: + return ResultFailure("expected an error, got nil") + case err.Error() != message: + return ResultFailure(fmt.Sprintf( + "expected error %q, got %s", message, formatErrorMessage(err))) + } + return ResultSuccess + } +} + +// ErrorContains succeeds if err is a non-nil error, and the error message contains +// the expected substring. +func ErrorContains(err error, substring string) Comparison { + return func() Result { + switch { + case err == nil: + return ResultFailure("expected an error, got nil") + case !strings.Contains(err.Error(), substring): + return ResultFailure(fmt.Sprintf( + "expected error to contain %q, got %s", substring, formatErrorMessage(err))) + } + return ResultSuccess + } +} + +type causer interface { + Cause() error +} + +func formatErrorMessage(err error) string { + // nolint: errorlint // unwrapping is not appropriate here + if _, ok := err.(causer); ok { + return fmt.Sprintf("%q\n%+v", err, err) + } + // This error was not wrapped with github.com/pkg/errors + return fmt.Sprintf("%q", err) +} + +// Nil succeeds if obj is a nil interface, pointer, or function. +// +// Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices, +// maps, and channels. +func Nil(obj interface{}) Comparison { + msgFunc := func(value reflect.Value) string { + return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type()) + } + return isNil(obj, msgFunc) +} + +func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison { + return func() Result { + if obj == nil { + return ResultSuccess + } + value := reflect.ValueOf(obj) + kind := value.Type().Kind() + if kind >= reflect.Chan && kind <= reflect.Slice { + if value.IsNil() { + return ResultSuccess + } + return ResultFailure(msgFunc(value)) + } + + return ResultFailure(fmt.Sprintf("%v (type %s) can not be nil", value, value.Type())) + } +} + +// ErrorType succeeds if err is not nil and is of the expected type. +// +// Expected can be one of: +// func(error) bool +// Function should return true if the error is the expected type. +// type struct{}, type &struct{} +// A struct or a pointer to a struct. +// Fails if the error is not of the same type as expected. +// type &interface{} +// A pointer to an interface type. +// Fails if err does not implement the interface. +// reflect.Type +// Fails if err does not implement the reflect.Type +func ErrorType(err error, expected interface{}) Comparison { + return func() Result { + switch expectedType := expected.(type) { + case func(error) bool: + return cmpErrorTypeFunc(err, expectedType) + case reflect.Type: + if expectedType.Kind() == reflect.Interface { + return cmpErrorTypeImplementsType(err, expectedType) + } + return cmpErrorTypeEqualType(err, expectedType) + case nil: + return ResultFailure("invalid type for expected: nil") + } + + expectedType := reflect.TypeOf(expected) + switch { + case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType): + return cmpErrorTypeEqualType(err, expectedType) + case isPtrToInterface(expectedType): + return cmpErrorTypeImplementsType(err, expectedType.Elem()) + } + return ResultFailure(fmt.Sprintf("invalid type for expected: %T", expected)) + } +} + +func cmpErrorTypeFunc(err error, f func(error) bool) Result { + if f(err) { + return ResultSuccess + } + actual := "nil" + if err != nil { + actual = fmt.Sprintf("%s (%T)", err, err) + } + return ResultFailureTemplate(`error is {{ .Data.actual }} + {{- with callArg 1 }}, not {{ formatNode . }}{{end -}}`, + map[string]interface{}{"actual": actual}) +} + +func cmpErrorTypeEqualType(err error, expectedType reflect.Type) Result { + if err == nil { + return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) + } + errValue := reflect.ValueOf(err) + if errValue.Type() == expectedType { + return ResultSuccess + } + return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) +} + +func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result { + if err == nil { + return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) + } + errValue := reflect.ValueOf(err) + if errValue.Type().Implements(expectedType) { + return ResultSuccess + } + return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) +} + +func isPtrToInterface(typ reflect.Type) bool { + return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface +} + +func isPtrToStruct(typ reflect.Type) bool { + return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct +} + +var ( + stdlibErrorNewType = reflect.TypeOf(errors.New("")) + stdlibFmtErrorType = reflect.TypeOf(fmt.Errorf("%w", fmt.Errorf(""))) +) + +// ErrorIs succeeds if errors.Is(actual, expected) returns true. See +// https://golang.org/pkg/errors/#Is for accepted argument values. +func ErrorIs(actual error, expected error) Comparison { + return func() Result { + if errors.Is(actual, expected) { + return ResultSuccess + } + + // The type of stdlib errors is excluded because the type is not relevant + // in those cases. The type is only important when it is a user defined + // custom error type. + return ResultFailureTemplate(`error is + {{- if not .Data.a }} nil,{{ else }} + {{- printf " \"%v\"" .Data.a }} + {{- if notStdlibErrorType .Data.a }} ({{ printf "%T" .Data.a }}){{ end }}, + {{- end }} not {{ printf "\"%v\"" .Data.x }} ( + {{- with callArg 1 }}{{ formatNode . }}{{ end }} + {{- if notStdlibErrorType .Data.x }}{{ printf " %T" .Data.x }}{{ end }})`, + map[string]interface{}{"a": actual, "x": expected}) + } +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/compare_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/compare_test.go new file mode 100644 index 0000000..e4546ea --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/compare_test.go @@ -0,0 +1,680 @@ +package cmp + +import ( + "errors" + "fmt" + "go/ast" + "io" + "os" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestDeepEqual(t *testing.T) { + t.Run("failure", func(t *testing.T) { + result := DeepEqual([]string{"a", "b"}, []string{"b", "a"})() + if result.Success() { + t.Errorf("expected failure") + } + + args := []ast.Expr{&ast.Ident{Name: "result"}, &ast.Ident{Name: "exp"}} + message := result.(templatedResult).FailureMessage(args) + expected := "\n--- result\n+++ exp\n" + if !strings.HasPrefix(message, expected) { + t.Errorf("expected prefix \n%q\ngot\n%q\n", expected, message) + } + }) + + t.Run("success", func(t *testing.T) { + actual := DeepEqual([]string{"a"}, []string{"a"})() + assertSuccess(t, actual) + }) +} + +type Stub struct { + unx int +} + +func TestDeepEqualWithUnexported(t *testing.T) { + result := DeepEqual(Stub{}, Stub{unx: 1})() + assertFailureHasPrefix(t, result, `cannot handle unexported field at {cmp.Stub}.unx:`) +} + +func TestRegexp(t *testing.T) { + var testcases = []struct { + name string + regex interface{} + value string + match bool + expErr string + }{ + { + name: "pattern string match", + regex: "^[0-9]+$", + value: "12123423456", + match: true, + }, + { + name: "simple pattern string no match", + regex: "bob", + value: "Probably", + expErr: `value "Probably" does not match regexp "bob"`, + }, + { + name: "pattern string no match", + regex: "^1", + value: "2123423456", + expErr: `value "2123423456" does not match regexp "^1"`, + }, + { + name: "regexp match", + regex: regexp.MustCompile("^d[0-9a-f]{8}$"), + value: "d1632beef", + match: true, + }, + { + name: "invalid regexp", + regex: "^1(", + value: "2", + expErr: "error parsing regexp: missing closing ): `^1(`", + }, + { + name: "invalid type", + regex: struct{}{}, + value: "some string", + expErr: "invalid type struct {} for regex pattern", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + res := Regexp(tc.regex, tc.value)() + if tc.match { + assertSuccess(t, res) + } else { + assertFailure(t, res, tc.expErr) + } + }) + } +} + +func TestLen(t *testing.T) { + var testcases = []struct { + seq interface{} + length int + expectedSuccess bool + expectedMessage string + }{ + { + seq: []string{"A", "b", "c"}, + length: 3, + expectedSuccess: true, + }, + { + seq: []string{"A", "b", "c"}, + length: 2, + expectedMessage: "expected [A b c] (length 3) to have length 2", + }, + { + seq: map[string]int{"a": 1, "b": 2}, + length: 2, + expectedSuccess: true, + }, + { + seq: [3]string{"a", "b", "c"}, + length: 3, + expectedSuccess: true, + }, + { + seq: "abcd", + length: 4, + expectedSuccess: true, + }, + { + seq: "abcd", + length: 3, + expectedMessage: "expected abcd (length 4) to have length 3", + }, + } + + for _, testcase := range testcases { + t.Run(fmt.Sprintf("%v len=%d", testcase.seq, testcase.length), func(t *testing.T) { + result := Len(testcase.seq, testcase.length)() + if testcase.expectedSuccess { + assertSuccess(t, result) + } else { + assertFailure(t, result, testcase.expectedMessage) + } + }) + } +} + +func TestPanics(t *testing.T) { + panicker := func() { + panic("AHHHHHHHHHHH") + } + + result := Panics(panicker)() + assertSuccess(t, result) + + result = Panics(func() {})() + assertFailure(t, result, "did not panic") +} + +type innerstub struct { + num int +} + +type stub struct { + stub innerstub + num int +} + +func TestDeepEqualEquivalenceToReflectDeepEqual(t *testing.T) { + var testcases = []struct { + left interface{} + right interface{} + }{ + {nil, nil}, + {7, 7}, + {false, false}, + {stub{innerstub{1}, 2}, stub{innerstub{1}, 2}}, + {[]int{1, 2, 3}, []int{1, 2, 3}}, + {[]byte(nil), []byte(nil)}, + {nil, []byte(nil)}, + {1, uint64(1)}, + {7, "7"}, + } + for _, testcase := range testcases { + expected := reflect.DeepEqual(testcase.left, testcase.right) + res := DeepEqual(testcase.left, testcase.right, cmpStub)() + if res.Success() != expected { + msg := res.(StringResult).FailureMessage() + t.Errorf("deepEqual(%v, %v) did not return %v (message %s)", + testcase.left, testcase.right, expected, msg) + } + } +} + +var cmpStub = cmp.AllowUnexported(stub{}, innerstub{}) + +func TestContains(t *testing.T) { + var testcases = []struct { + seq interface{} + item interface{} + expected bool + expectedMsg string + }{ + { + seq: error(nil), + item: 0, + expectedMsg: "nil does not contain items", + }, + { + seq: "abcdef", + item: "cde", + expected: true, + }, + { + seq: "abcdef", + item: "foo", + expectedMsg: `string "abcdef" does not contain "foo"`, + }, + { + seq: "abcdef", + item: 3, + expectedMsg: `string may only contain strings`, + }, + { + seq: map[rune]int{'a': 1, 'b': 2}, + item: 'b', + expected: true, + }, + { + seq: map[rune]int{'a': 1}, + item: 'c', + expectedMsg: "map[97:1] does not contain 99", + }, + { + seq: map[int]int{'a': 1, 'b': 2}, + item: 'b', + expectedMsg: "map[int]int can not contain a int32 key", + }, + { + seq: []interface{}{"a", 1, 'a', 1.0, true}, + item: 'a', + expected: true, + }, + { + seq: []interface{}{"a", 1, 'a', 1.0, true}, + item: 3, + expectedMsg: "[a 1 97 1 true] does not contain 3", + }, + { + seq: [3]byte{99, 10, 100}, + item: byte(99), + expected: true, + }, + { + seq: [3]byte{99, 10, 100}, + item: byte(98), + expectedMsg: "[99 10 100] does not contain 98", + }, + } + for _, testcase := range testcases { + name := fmt.Sprintf("%v in %v", testcase.item, testcase.seq) + t.Run(name, func(t *testing.T) { + result := Contains(testcase.seq, testcase.item)() + if testcase.expected { + assertSuccess(t, result) + } else { + assertFailure(t, result, testcase.expectedMsg) + } + }) + } +} + +func TestEqualMultiLine(t *testing.T) { + result := `abcd +1234 +aaaa +bbbb` + + exp := `abcd +1111 +aaaa +bbbb` + + expected := ` +--- result ++++ exp +@@ -1,4 +1,4 @@ + abcd +-1234 ++1111 + aaaa + bbbb +` + + args := []ast.Expr{&ast.Ident{Name: "result"}, &ast.Ident{Name: "exp"}} + res := Equal(result, exp)() + assertFailureTemplate(t, res, args, expected) +} + +func TestEqual_PointersNotEqual(t *testing.T) { + x := 123 + y := 123 + + res := Equal(&x, &y)() + args := []ast.Expr{&ast.Ident{Name: "x"}, &ast.Ident{Name: "y"}} + expected := fmt.Sprintf("%p (x *int) != %p (y *int)", &x, &y) + assertFailureTemplate(t, res, args, expected) +} + +// errorWithCause mimics the error formatting of github.com/pkg/errors +type errorWithCause struct { + msg string + cause error +} + +func (e errorWithCause) Error() string { + return fmt.Sprintf("%v with cause: %v", e.msg, e.cause) +} + +func (e errorWithCause) Cause() error { + return e.cause +} + +func (e errorWithCause) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", e.Cause()) + fmt.Fprint(s, "\nstack trace") + return + } + fallthrough + case 's': + io.WriteString(s, e.Error()) + case 'q': + fmt.Fprintf(s, "%q", e.Error()) + } +} + +func TestError(t *testing.T) { + result := Error(nil, "the error message")() + assertFailure(t, result, "expected an error, got nil") + + // A Wrapped error also includes the stack + result = Error(errorWithCause{cause: errors.New("other"), msg: "wrapped"}, "the error message")() + assertFailureHasPrefix(t, result, + `expected error "the error message", got "wrapped with cause: other" +other +stack trace`) + + msg := "the message" + result = Error(errors.New(msg), msg)() + assertSuccess(t, result) +} + +func TestErrorContains(t *testing.T) { + result := ErrorContains(nil, "the error message")() + assertFailure(t, result, "expected an error, got nil") + + result = ErrorContains(errors.New("other"), "the error")() + assertFailureHasPrefix(t, result, + `expected error to contain "the error", got "other"`) + + msg := "the full message" + result = ErrorContains(errors.New(msg), "full")() + assertSuccess(t, result) +} + +func TestNil(t *testing.T) { + result := Nil(nil)() + assertSuccess(t, result) + + var s *string + result = Nil(s)() + assertSuccess(t, result) + + var closer io.Closer + result = Nil(closer)() + assertSuccess(t, result) + + result = Nil("wrong")() + assertFailure(t, result, "wrong (type string) can not be nil") + + notnil := "notnil" + result = Nil(¬nil)() + assertFailure(t, result, "notnil (type *string) is not nil") + + result = Nil([]string{"a"})() + assertFailure(t, result, "[a] (type []string) is not nil") +} + +type testingT interface { + Errorf(msg string, args ...interface{}) +} + +type helperT interface { + Helper() +} + +func assertSuccess(t testingT, res Result) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !res.Success() { + msg := res.(StringResult).FailureMessage() + t.Errorf("expected success, but got failure with message %q", msg) + } +} + +func assertFailure(t testingT, res Result, expected string) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if res.Success() { + t.Errorf("expected failure") + } + message := res.(StringResult).FailureMessage() + if message != expected { + t.Errorf("expected \n%q\ngot\n%q\n", expected, message) + } +} + +func assertFailureHasPrefix(t testingT, res Result, prefix string) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if res.Success() { + t.Errorf("expected failure") + } + message := res.(StringResult).FailureMessage() + if !strings.HasPrefix(message, prefix) { + t.Errorf("expected \n%v\nto start with\n%v\n", message, prefix) + } +} + +func assertFailureTemplate(t testingT, res Result, args []ast.Expr, expected string) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if res.Success() { + t.Errorf("expected failure") + } + message := res.(templatedResult).FailureMessage(args) + if message != expected { + t.Errorf("expected \n%q\ngot\n%q\n", expected, message) + } +} + +type stubError struct{} + +func (s stubError) Error() string { + return "stub error" +} + +func isErrorOfTypeStub(err error) bool { + return reflect.TypeOf(err) == reflect.TypeOf(stubError{}) +} + +type notStubError struct{} + +func (s notStubError) Error() string { + return "not stub error" +} + +func isErrorOfTypeNotStub(err error) bool { + return reflect.TypeOf(err) == reflect.TypeOf(notStubError{}) +} + +type specialStubIface interface { + Special() +} + +type stubPtrError struct{} + +func (s *stubPtrError) Error() string { + return "stub ptr error" +} + +func TestErrorTypeWithNil(t *testing.T) { + var testcases = []struct { + name string + expType interface{} + expected string + }{ + { + name: "with struct", + expType: stubError{}, + expected: "error is nil, not cmp.stubError", + }, + { + name: "with pointer to struct", + expType: &stubPtrError{}, + expected: "error is nil, not *cmp.stubPtrError", + }, + { + name: "with interface", + expType: (*specialStubIface)(nil), + expected: "error is nil, not cmp.specialStubIface", + }, + { + name: "with reflect.Type", + expType: reflect.TypeOf(stubError{}), + expected: "error is nil, not cmp.stubError", + }, + } + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + result := ErrorType(nil, testcase.expType)() + assertFailure(t, result, testcase.expected) + }) + } +} + +func TestErrorTypeSuccess(t *testing.T) { + var testcases = []struct { + name string + expType interface{} + err error + }{ + { + name: "with function", + expType: isErrorOfTypeStub, + err: stubError{}, + }, + { + name: "with struct", + expType: stubError{}, + err: stubError{}, + }, + { + name: "with pointer to struct", + expType: &stubPtrError{}, + err: &stubPtrError{}, + }, + { + name: "with interface", + expType: (*error)(nil), + err: stubError{}, + }, + { + name: "with reflect.Type struct", + expType: reflect.TypeOf(stubError{}), + err: stubError{}, + }, + { + name: "with reflect.Type interface", + expType: reflect.TypeOf((*error)(nil)).Elem(), + err: stubError{}, + }, + } + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + result := ErrorType(testcase.err, testcase.expType)() + assertSuccess(t, result) + }) + } +} + +func TestErrorTypeFailure(t *testing.T) { + var testcases = []struct { + name string + expType interface{} + expected string + }{ + { + name: "with struct", + expType: notStubError{}, + expected: "error is stub error (cmp.stubError), not cmp.notStubError", + }, + { + name: "with pointer to struct", + expType: &stubPtrError{}, + expected: "error is stub error (cmp.stubError), not *cmp.stubPtrError", + }, + { + name: "with interface", + expType: (*specialStubIface)(nil), + expected: "error is stub error (cmp.stubError), not cmp.specialStubIface", + }, + { + name: "with reflect.Type struct", + expType: reflect.TypeOf(notStubError{}), + expected: "error is stub error (cmp.stubError), not cmp.notStubError", + }, + { + name: "with reflect.Type interface", + expType: reflect.TypeOf((*specialStubIface)(nil)).Elem(), + expected: "error is stub error (cmp.stubError), not cmp.specialStubIface", + }, + } + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + result := ErrorType(stubError{}, testcase.expType)() + assertFailure(t, result, testcase.expected) + }) + } +} + +func TestErrorTypeInvalid(t *testing.T) { + result := ErrorType(stubError{}, nil)() + assertFailure(t, result, "invalid type for expected: nil") + + result = ErrorType(stubError{}, "my type!")() + assertFailure(t, result, "invalid type for expected: string") +} + +func TestErrorTypeWithFunc(t *testing.T) { + result := ErrorType(nil, isErrorOfTypeStub)() + assertFailureTemplate(t, result, + []ast.Expr{nil, &ast.Ident{Name: "isErrorOfTypeStub"}}, + "error is nil, not isErrorOfTypeStub") + + result = ErrorType(stubError{}, isErrorOfTypeNotStub)() + assertFailureTemplate(t, result, + []ast.Expr{nil, &ast.Ident{Name: "isErrorOfTypeNotStub"}}, + "error is stub error (cmp.stubError), not isErrorOfTypeNotStub") +} + +func TestErrorIs(t *testing.T) { + t.Run("equal", func(t *testing.T) { + result := ErrorIs(stubError{}, stubError{})() + assertSuccess(t, result) + }) + t.Run("actual is nil, not stdlib error", func(t *testing.T) { + result := ErrorIs(nil, stubError{})() + args := []ast.Expr{ + &ast.Ident{Name: "err"}, + &ast.SelectorExpr{ + X: &ast.Ident{Name: "mypkg"}, + Sel: &ast.Ident{Name: "StubError"}, + }, + } + expected := `error is nil, not "stub error" (mypkg.StubError cmp.stubError)` + assertFailureTemplate(t, result, args, expected) + }) + t.Run("not equal, not stdlib error", func(t *testing.T) { + result := ErrorIs(notStubError{}, stubError{})() + args := []ast.Expr{ + &ast.Ident{Name: "err"}, + &ast.SelectorExpr{ + X: &ast.Ident{Name: "mypkg"}, + Sel: &ast.Ident{Name: "StubError"}, + }, + } + expected := `error is "not stub error" (cmp.notStubError), not "stub error" (mypkg.StubError cmp.stubError)` + assertFailureTemplate(t, result, args, expected) + }) + t.Run("actual is nil, stdlib error", func(t *testing.T) { + result := ErrorIs(nil, os.ErrClosed)() + args := []ast.Expr{ + &ast.Ident{Name: "err"}, + &ast.SelectorExpr{ + X: &ast.Ident{Name: "os"}, + Sel: &ast.Ident{Name: "ErrClosed"}, + }, + } + expected := `error is nil, not "file already closed" (os.ErrClosed)` + assertFailureTemplate(t, result, args, expected) + }) + t.Run("not equal, stdlib error", func(t *testing.T) { + result := ErrorIs(fmt.Errorf("foo"), os.ErrClosed)() + args := []ast.Expr{ + &ast.Ident{Name: "err"}, + &ast.SelectorExpr{ + X: &ast.Ident{Name: "os"}, + Sel: &ast.Ident{Name: "ErrClosed"}, + }, + } + expected := `error is "foo", not "file already closed" (os.ErrClosed)` + assertFailureTemplate(t, result, args, expected) + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/result.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/result.go new file mode 100644 index 0000000..28ef8d3 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/cmp/result.go @@ -0,0 +1,110 @@ +package cmp + +import ( + "bytes" + "fmt" + "go/ast" + "reflect" + "text/template" + + "gotest.tools/v3/internal/source" +) + +// A Result of a Comparison. +type Result interface { + Success() bool +} + +// StringResult is an implementation of Result that reports the error message +// string verbatim and does not provide any templating or formatting of the +// message. +type StringResult struct { + success bool + message string +} + +// Success returns true if the comparison was successful. +func (r StringResult) Success() bool { + return r.success +} + +// FailureMessage returns the message used to provide additional information +// about the failure. +func (r StringResult) FailureMessage() string { + return r.message +} + +// ResultSuccess is a constant which is returned by a ComparisonWithResult to +// indicate success. +var ResultSuccess = StringResult{success: true} + +// ResultFailure returns a failed Result with a failure message. +func ResultFailure(message string) StringResult { + return StringResult{message: message} +} + +// ResultFromError returns ResultSuccess if err is nil. Otherwise ResultFailure +// is returned with the error message as the failure message. +func ResultFromError(err error) Result { + if err == nil { + return ResultSuccess + } + return ResultFailure(err.Error()) +} + +type templatedResult struct { + template string + data map[string]interface{} +} + +func (r templatedResult) Success() bool { + return false +} + +func (r templatedResult) FailureMessage(args []ast.Expr) string { + msg, err := renderMessage(r, args) + if err != nil { + return fmt.Sprintf("failed to render failure message: %s", err) + } + return msg +} + +func (r templatedResult) UpdatedExpected(stackIndex int) error { + // TODO: would be nice to have structured data instead of a map + return source.UpdateExpectedValue(stackIndex+1, r.data["x"], r.data["y"]) +} + +// ResultFailureTemplate returns a Result with a template string and data which +// can be used to format a failure message. The template may access data from .Data, +// the comparison args with the callArg function, and the formatNode function may +// be used to format the call args. +func ResultFailureTemplate(template string, data map[string]interface{}) Result { + return templatedResult{template: template, data: data} +} + +func renderMessage(result templatedResult, args []ast.Expr) (string, error) { + tmpl := template.New("failure").Funcs(template.FuncMap{ + "formatNode": source.FormatNode, + "callArg": func(index int) ast.Expr { + if index >= len(args) { + return nil + } + return args[index] + }, + // TODO: any way to include this from ErrorIS instead of here? + "notStdlibErrorType": func(typ interface{}) bool { + r := reflect.TypeOf(typ) + return r != stdlibFmtErrorType && r != stdlibErrorNewType + }, + }) + var err error + tmpl, err = tmpl.Parse(result.template) + if err != nil { + return "", err + } + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, map[string]interface{}{ + "Data": result.data, + }) + return buf.String(), err +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/example_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/example_test.go new file mode 100644 index 0000000..2abfe12 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/example_test.go @@ -0,0 +1,26 @@ +package assert_test + +import ( + "fmt" + "regexp" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +var t = &testing.T{} + +func ExampleAssert_customComparison() { + regexPattern := func(value string, pattern string) cmp.Comparison { + return func() cmp.Result { + re := regexp.MustCompile(pattern) + if re.MatchString(value) { + return cmp.ResultSuccess + } + return cmp.ResultFailure( + fmt.Sprintf("%q did not match pattern %q", value, pattern)) + } + } + assert.Assert(t, regexPattern("12345.34", `\d+.\d\d`)) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/opt/opt.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/opt/opt.go new file mode 100644 index 0000000..357cdf2 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/opt/opt.go @@ -0,0 +1,118 @@ +/*Package opt provides common go-cmp.Options for use with assert.DeepEqual. + */ +package opt // import "gotest.tools/v3/assert/opt" + +import ( + "fmt" + "reflect" + "strings" + "time" + + gocmp "github.com/google/go-cmp/cmp" +) + +// DurationWithThreshold returns a gocmp.Comparer for comparing time.Duration. The +// Comparer returns true if the difference between the two Duration values is +// within the threshold and neither value is zero. +func DurationWithThreshold(threshold time.Duration) gocmp.Option { + return gocmp.Comparer(cmpDuration(threshold)) +} + +func cmpDuration(threshold time.Duration) func(x, y time.Duration) bool { + return func(x, y time.Duration) bool { + if x == 0 || y == 0 { + return false + } + delta := x - y + return delta <= threshold && delta >= -threshold + } +} + +// TimeWithThreshold returns a gocmp.Comparer for comparing time.Time. The +// Comparer returns true if the difference between the two Time values is +// within the threshold and neither value is zero. +func TimeWithThreshold(threshold time.Duration) gocmp.Option { + return gocmp.Comparer(cmpTime(threshold)) +} + +func cmpTime(threshold time.Duration) func(x, y time.Time) bool { + return func(x, y time.Time) bool { + if x.IsZero() || y.IsZero() { + return false + } + delta := x.Sub(y) + return delta <= threshold && delta >= -threshold + } +} + +// PathString is a gocmp.FilterPath filter that returns true when path.String() +// matches any of the specs. +// +// The path spec is a dot separated string where each segment is a field name. +// Slices, Arrays, and Maps are always matched against every element in the +// sequence. gocmp.Indirect, gocmp.Transform, and gocmp.TypeAssertion are always +// ignored. +// +// Note: this path filter is not type safe. Incorrect paths will be silently +// ignored. Consider using a type safe path filter for more complex paths. +func PathString(specs ...string) func(path gocmp.Path) bool { + return func(path gocmp.Path) bool { + for _, spec := range specs { + if path.String() == spec { + return true + } + } + return false + } +} + +// PathDebug is a gocmp.FilerPath filter that always returns false. It prints +// each path it receives. It can be used to debug path matching problems. +func PathDebug(path gocmp.Path) bool { + fmt.Printf("PATH string=%s gostring=%s\n", path, path.GoString()) + for _, step := range path { + fmt.Printf(" STEP %s\ttype=%s\t%s\n", + formatStepType(step), step.Type(), stepTypeFields(step)) + } + return false +} + +func formatStepType(step gocmp.PathStep) string { + return strings.Title(strings.TrimPrefix(reflect.TypeOf(step).String(), "*cmp.")) +} + +func stepTypeFields(step gocmp.PathStep) string { + switch typed := step.(type) { + case gocmp.StructField: + return fmt.Sprintf("name=%s", typed.Name()) + case gocmp.MapIndex: + return fmt.Sprintf("key=%s", typed.Key().Interface()) + case gocmp.Transform: + return fmt.Sprintf("name=%s", typed.Name()) + case gocmp.SliceIndex: + return fmt.Sprintf("name=%d", typed.Key()) + } + return "" +} + +// PathField is a gocmp.FilerPath filter that matches a struct field by name. +// PathField will match every instance of the field in a recursive or nested +// structure. +func PathField(structType interface{}, field string) func(gocmp.Path) bool { + typ := reflect.TypeOf(structType) + if typ.Kind() != reflect.Struct { + panic(fmt.Sprintf("type %s is not a struct", typ)) + } + if _, ok := typ.FieldByName(field); !ok { + panic(fmt.Sprintf("type %s does not have field %s", typ, field)) + } + + return func(path gocmp.Path) bool { + return path.Index(-2).Type() == typ && isStructField(path.Index(-1), field) + } +} + +func isStructField(step gocmp.PathStep, name string) bool { + field, ok := step.(gocmp.StructField) + return ok && field.Name() == name +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/opt/opt_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/opt/opt_test.go new file mode 100644 index 0000000..c036d18 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/assert/opt/opt_test.go @@ -0,0 +1,265 @@ +package opt + +import ( + "sort" + "testing" + "time" + + gocmp "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert" +) + +func TestDurationWithThreshold(t *testing.T) { + var testcases = []struct { + name string + x, y, threshold time.Duration + expected bool + }{ + { + name: "delta is threshold", + threshold: time.Second, + x: 3 * time.Second, + y: 2 * time.Second, + expected: true, + }, + { + name: "delta is negative threshold", + threshold: time.Second, + x: 2 * time.Second, + y: 3 * time.Second, + expected: true, + }, + { + name: "delta within threshold", + threshold: time.Second, + x: 300 * time.Millisecond, + y: 100 * time.Millisecond, + expected: true, + }, + { + name: "delta within negative threshold", + threshold: time.Second, + x: 100 * time.Millisecond, + y: 300 * time.Millisecond, + expected: true, + }, + { + name: "delta outside threshold", + threshold: time.Second, + x: 5 * time.Second, + y: 300 * time.Millisecond, + }, + { + name: "delta outside negative threshold", + threshold: time.Second, + x: 300 * time.Millisecond, + y: 5 * time.Second, + }, + { + name: "x is 0", + threshold: time.Second, + y: 5 * time.Millisecond, + }, + { + name: "y is 0", + threshold: time.Second, + x: 5 * time.Millisecond, + }, + } + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + actual := cmpDuration(testcase.threshold)(testcase.x, testcase.y) + assert.Equal(t, actual, testcase.expected) + }) + } +} + +func TestTimeWithThreshold(t *testing.T) { + var now = time.Now() + + var testcases = []struct { + name string + x, y time.Time + threshold time.Duration + expected bool + }{ + { + name: "delta is threshold", + threshold: time.Minute, + x: now, + y: now.Add(time.Minute), + expected: true, + }, + { + name: "delta is negative threshold", + threshold: time.Minute, + x: now, + y: now.Add(-time.Minute), + expected: true, + }, + { + name: "delta within threshold", + threshold: time.Hour, + x: now, + y: now.Add(time.Minute), + expected: true, + }, + { + name: "delta within negative threshold", + threshold: time.Hour, + x: now, + y: now.Add(-time.Minute), + expected: true, + }, + { + name: "delta outside threshold", + threshold: time.Second, + x: now, + y: now.Add(time.Minute), + }, + { + name: "delta outside negative threshold", + threshold: time.Second, + x: now, + y: now.Add(-time.Minute), + }, + { + name: "x is 0", + threshold: time.Second, + y: now, + }, + { + name: "y is 0", + threshold: time.Second, + x: now, + }, + } + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + actual := cmpTime(testcase.threshold)(testcase.x, testcase.y) + assert.Equal(t, actual, testcase.expected) + }) + } +} + +type node struct { + Value nodeValue + Labels map[string]node + Children []node + Ref *node +} + +type nodeValue struct { + Value int +} + +type pathRecorder struct { + filter func(p gocmp.Path) bool + matches []string +} + +func (p *pathRecorder) record(path gocmp.Path) bool { + if p.filter(path) { + p.matches = append(p.matches, path.GoString()) + } + return false +} + +func matchPaths(fixture interface{}, filter func(gocmp.Path) bool) []string { + rec := &pathRecorder{filter: filter} + gocmp.Equal(fixture, fixture, gocmp.FilterPath(rec.record, gocmp.Ignore())) + sort.Strings(rec.matches) + return rec.matches +} + +func TestPathStringFromStruct(t *testing.T) { + fixture := node{ + Ref: &node{ + Children: []node{ + {}, + { + Labels: map[string]node{ + "first": {Value: nodeValue{Value: 3}}, + }, + }, + }, + }, + } + + spec := "Ref.Children.Labels.Value" + matches := matchPaths(fixture, PathString(spec)) + expected := []string{ + `{opt.node}.Ref.Children[1].Labels["first"].Value`, + `{opt.node}.Ref.Children[1].Labels["first"].Value`, + } + assert.DeepEqual(t, matches, expected) +} + +func TestPathStringFromSlice(t *testing.T) { + fixture := []node{ + { + Ref: &node{ + Children: []node{ + {}, + { + Labels: map[string]node{ + "first": {}, + "second": { + Ref: &node{Value: nodeValue{Value: 3}}, + }, + }, + }, + }, + }, + }, + } + + spec := "Ref.Children.Labels.Ref.Value" + matches := matchPaths(fixture, PathString(spec)) + expected := []string{ + `{[]opt.node}[0].Ref.Children[1].Labels["second"].Ref.Value`, + `{[]opt.node}[0].Ref.Children[1].Labels["second"].Ref.Value`, + `{[]opt.node}[0].Ref.Children[1].Labels["second"].Ref.Value`, + `{[]opt.node}[0].Ref.Children[1].Labels["second"].Ref.Value`, + } + assert.DeepEqual(t, matches, expected) +} + +func TestPathField(t *testing.T) { + fixture := node{ + Value: nodeValue{Value: 3}, + Children: []node{ + {}, + {Value: nodeValue{Value: 2}}, + {Ref: &node{Value: nodeValue{Value: 9}}}, + }, + } + + filter := PathField(nodeValue{}, "Value") + matches := matchPaths(fixture, filter) + expected := []string{ + "{opt.node}.Children[0].Value.Value", + "{opt.node}.Children[0].Value.Value", + "{opt.node}.Children[1].Value.Value", + "{opt.node}.Children[1].Value.Value", + "{opt.node}.Children[2].Ref.Value.Value", + "{opt.node}.Children[2].Ref.Value.Value", + "{opt.node}.Children[2].Value.Value", + "{opt.node}.Children[2].Value.Value", + "{opt.node}.Value.Value", + } + assert.DeepEqual(t, matches, expected) +} + +func TestPathDebug(t *testing.T) { + fixture := node{ + Value: nodeValue{Value: 3}, + Children: []node{ + {Ref: &node{Value: nodeValue{Value: 9}}}, + }, + Labels: map[string]node{ + "label1": {}, + }, + } + gocmp.Equal(fixture, fixture, gocmp.FilterPath(PathDebug, gocmp.Ignore())) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/env/env.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/env/env.go new file mode 100644 index 0000000..a06eab3 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/env/env.go @@ -0,0 +1,120 @@ +/*Package env provides functions to test code that read environment variables +or the current working directory. +*/ +package env // import "gotest.tools/v3/env" + +import ( + "os" + "strings" + + "gotest.tools/v3/assert" + "gotest.tools/v3/internal/cleanup" +) + +type helperT interface { + Helper() +} + +// Patch changes the value of an environment variable, and returns a +// function which will reset the the value of that variable back to the +// previous state. +// +// When used with Go 1.14+ the unpatch function will be called automatically +// when the test ends, unless the TEST_NOCLEANUP env var is set to true. +// +// Deprecated: use t.SetEnv +func Patch(t assert.TestingT, key, value string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + oldValue, envVarExists := os.LookupEnv(key) + assert.NilError(t, os.Setenv(key, value)) + clean := func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !envVarExists { + assert.NilError(t, os.Unsetenv(key)) + return + } + assert.NilError(t, os.Setenv(key, oldValue)) + } + cleanup.Cleanup(t, clean) + return clean +} + +// PatchAll sets the environment to env, and returns a function which will +// reset the environment back to the previous state. +// +// When used with Go 1.14+ the unpatch function will be called automatically +// when the test ends, unless the TEST_NOCLEANUP env var is set to true. +func PatchAll(t assert.TestingT, env map[string]string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + oldEnv := os.Environ() + os.Clearenv() + + for key, value := range env { + assert.NilError(t, os.Setenv(key, value), "setenv %s=%s", key, value) + } + clean := func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + os.Clearenv() + for key, oldVal := range ToMap(oldEnv) { + assert.NilError(t, os.Setenv(key, oldVal), "setenv %s=%s", key, oldVal) + } + } + cleanup.Cleanup(t, clean) + return clean +} + +// ToMap takes a list of strings in the format returned by os.Environ() and +// returns a mapping of keys to values. +func ToMap(env []string) map[string]string { + result := map[string]string{} + for _, raw := range env { + key, value := getParts(raw) + result[key] = value + } + return result +} + +func getParts(raw string) (string, string) { + if raw == "" { + return "", "" + } + // Environment variables on windows can begin with = + // http://blogs.msdn.com/b/oldnewthing/archive/2010/05/06/10008132.aspx + parts := strings.SplitN(raw[1:], "=", 2) + key := raw[:1] + parts[0] + if len(parts) == 1 { + return key, "" + } + return key, parts[1] +} + +// ChangeWorkingDir to the directory, and return a function which restores the +// previous working directory. +// +// When used with Go 1.14+ the previous working directory will be restored +// automatically when the test ends, unless the TEST_NOCLEANUP env var is set to +// true. +func ChangeWorkingDir(t assert.TestingT, dir string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + cwd, err := os.Getwd() + assert.NilError(t, err) + assert.NilError(t, os.Chdir(dir)) + clean := func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.NilError(t, os.Chdir(cwd)) + } + cleanup.Cleanup(t, clean) + return clean +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/env/env_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/env/env_test.go new file mode 100644 index 0000000..54458f2 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/env/env_test.go @@ -0,0 +1,170 @@ +package env + +import ( + "os" + "runtime" + "sort" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" + "gotest.tools/v3/internal/source" + "gotest.tools/v3/skip" +) + +func TestPatchFromUnset(t *testing.T) { + key, value := "FOO_IS_UNSET", "VALUE" + revert := Patch(t, key, value) + + assert.Assert(t, value == os.Getenv(key)) + revert() + _, isSet := os.LookupEnv(key) + assert.Assert(t, !isSet) +} + +func TestPatch(t *testing.T) { + skip.If(t, os.Getenv("PATH") == "") + oldVal := os.Getenv("PATH") + + key, value := "PATH", "NEWVALUE" + revert := Patch(t, key, value) + + assert.Assert(t, value == os.Getenv(key)) + revert() + assert.Assert(t, oldVal == os.Getenv(key)) +} + +func TestPatch_IntegrationWithCleanup(t *testing.T) { + skip.If(t, source.GoVersionLessThan(1, 14)) + + key := "totally_unique_env_var_key" + t.Run("cleanup in subtest", func(t *testing.T) { + Patch(t, key, "the-new-value") + assert.Equal(t, os.Getenv(key), "the-new-value") + }) + + t.Run("env var is unset", func(t *testing.T) { + v, ok := os.LookupEnv(key) + assert.Assert(t, !ok, "expected env var to be unset, got %v", v) + }) +} + +func TestPatchAll(t *testing.T) { + oldEnv := os.Environ() + newEnv := map[string]string{ + "FIRST": "STARS", + "THEN": "MOON", + } + + revert := PatchAll(t, newEnv) + + actual := os.Environ() + sort.Strings(actual) + assert.DeepEqual(t, []string{"FIRST=STARS", "THEN=MOON"}, actual) + + revert() + assert.DeepEqual(t, sorted(oldEnv), sorted(os.Environ())) +} + +func TestPatchAllWindows(t *testing.T) { + skip.If(t, runtime.GOOS != "windows") + oldEnv := os.Environ() + newEnv := map[string]string{ + "FIRST": "STARS", + "THEN": "MOON", + "=FINAL": "SUN", + "=BAR": "", + } + + revert := PatchAll(t, newEnv) + + actual := os.Environ() + sort.Strings(actual) + assert.DeepEqual(t, []string{"=BAR=", "=FINAL=SUN", "FIRST=STARS", "THEN=MOON"}, actual) + + revert() + assert.DeepEqual(t, sorted(oldEnv), sorted(os.Environ())) +} + +func sorted(source []string) []string { + sort.Strings(source) + return source +} + +func TestPatchAll_IntegrationWithCleanup(t *testing.T) { + skip.If(t, source.GoVersionLessThan(1, 14)) + + key := "totally_unique_env_var_key" + t.Run("cleanup in subtest", func(t *testing.T) { + PatchAll(t, map[string]string{key: "the-new-value"}) + assert.Equal(t, os.Getenv(key), "the-new-value") + }) + + t.Run("env var is unset", func(t *testing.T) { + v, ok := os.LookupEnv(key) + assert.Assert(t, !ok, "expected env var to be unset, got %v", v) + }) +} + +func TestToMap(t *testing.T) { + source := []string{ + "key=value", + "novaluekey", + "=foo=bar", + "z=singlecharkey", + "b", + "", + } + actual := ToMap(source) + expected := map[string]string{ + "key": "value", + "novaluekey": "", + "=foo": "bar", + "z": "singlecharkey", + "b": "", + "": "", + } + assert.DeepEqual(t, expected, actual) +} + +func TestChangeWorkingDir(t *testing.T) { + tmpDir := fs.NewDir(t, t.Name()) + defer tmpDir.Remove() + + origWorkDir := pwd(t) + + reset := ChangeWorkingDir(t, tmpDir.Path()) + t.Run("changed to dir", func(t *testing.T) { + assert.Equal(t, pwd(t), tmpDir.Path()) + }) + + t.Run("reset dir", func(t *testing.T) { + reset() + assert.Equal(t, pwd(t), origWorkDir) + }) +} + +func TestChangeWorkingDir_IntegrationWithCleanup(t *testing.T) { + skip.If(t, source.GoVersionLessThan(1, 14)) + + tmpDir := fs.NewDir(t, t.Name()) + defer tmpDir.Remove() + + origWorkDir := pwd(t) + + t.Run("cleanup in subtest", func(t *testing.T) { + ChangeWorkingDir(t, tmpDir.Path()) + assert.Equal(t, pwd(t), tmpDir.Path()) + }) + + t.Run("working dir is reset", func(t *testing.T) { + assert.Equal(t, pwd(t), origWorkDir) + }) +} + +func pwd(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + assert.NilError(t, err) + return dir +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/env/example_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/env/example_test.go new file mode 100644 index 0000000..b2d2f41 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/env/example_test.go @@ -0,0 +1,18 @@ +package env + +import "testing" + +var t = &testing.T{} + +// Patch an environment variable and defer to return to the previous state +func ExamplePatch() { + defer Patch(t, "PATH", "/custom/path")() +} + +// Patch all environment variables +func ExamplePatchAll() { + defer PatchAll(t, map[string]string{ + "ONE": "FOO", + "TWO": "BAR", + })() +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/example_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/example_test.go new file mode 100644 index 0000000..09ea38d --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/example_test.go @@ -0,0 +1,70 @@ +package fs_test + +import ( + "io/ioutil" + "os" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/fs" + "gotest.tools/v3/golden" +) + +var t = &testing.T{} + +// Create a temporary directory which contains a single file +func ExampleNewDir() { + dir := fs.NewDir(t, "test-name", fs.WithFile("file1", "content\n")) + defer dir.Remove() + + files, err := ioutil.ReadDir(dir.Path()) + assert.NilError(t, err) + assert.Assert(t, cmp.Len(files, 0)) +} + +// Create a new file with some content +func ExampleNewFile() { + file := fs.NewFile(t, "test-name", fs.WithContent("content\n"), fs.AsUser(0, 0)) + defer file.Remove() + + content, err := ioutil.ReadFile(file.Path()) + assert.NilError(t, err) + assert.Equal(t, "content\n", content) +} + +// Create a directory and subdirectory with files +func ExampleWithDir() { + dir := fs.NewDir(t, "test-name", + fs.WithDir("subdir", + fs.WithMode(os.FileMode(0700)), + fs.WithFile("file1", "content\n")), + ) + defer dir.Remove() +} + +// Test that a directory contains the expected files, and all the files have the +// expected properties. +func ExampleEqual() { + path := operationWhichCreatesFiles() + expected := fs.Expected(t, + fs.WithFile("one", "", + fs.WithBytes(golden.Get(t, "one.golden")), + fs.WithMode(0600)), + fs.WithDir("data", + fs.WithFile("config", "", fs.MatchAnyFileContent))) + + assert.Assert(t, fs.Equal(path, expected)) +} + +func operationWhichCreatesFiles() string { + return "example-path" +} + +// Add a file to an existing directory +func ExampleApply() { + dir := fs.NewDir(t, "test-name") + defer dir.Remove() + + fs.Apply(t, dir, fs.WithFile("file1", "content\n")) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/file.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/file.go new file mode 100644 index 0000000..3ca5660 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/file.go @@ -0,0 +1,131 @@ +/*Package fs provides tools for creating temporary files, and testing the +contents and structure of a directory. +*/ +package fs // import "gotest.tools/v3/fs" + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + + "gotest.tools/v3/assert" + "gotest.tools/v3/internal/cleanup" +) + +// Path objects return their filesystem path. Path may be implemented by a +// real filesystem object (such as File and Dir) or by a type which updates +// entries in a Manifest. +type Path interface { + Path() string + Remove() +} + +var ( + _ Path = &Dir{} + _ Path = &File{} +) + +// File is a temporary file on the filesystem +type File struct { + path string +} + +type helperT interface { + Helper() +} + +// NewFile creates a new file in a temporary directory using prefix as part of +// the filename. The PathOps are applied to the before returning the File. +// +// When used with Go 1.14+ the file will be automatically removed when the test +// ends, unless the TEST_NOCLEANUP env var is set to true. +func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + tempfile, err := ioutil.TempFile("", cleanPrefix(prefix)+"-") + assert.NilError(t, err) + + file := &File{path: tempfile.Name()} + cleanup.Cleanup(t, file.Remove) + + assert.NilError(t, tempfile.Close()) + assert.NilError(t, applyPathOps(file, ops)) + return file +} + +func cleanPrefix(prefix string) string { + // windows requires both / and \ are replaced + if runtime.GOOS == "windows" { + prefix = strings.Replace(prefix, string(os.PathSeparator), "-", -1) + } + return strings.Replace(prefix, "/", "-", -1) +} + +// Path returns the full path to the file +func (f *File) Path() string { + return f.path +} + +// Remove the file +func (f *File) Remove() { + // nolint: errcheck + os.Remove(f.path) +} + +// Dir is a temporary directory +type Dir struct { + path string +} + +// NewDir returns a new temporary directory using prefix as part of the directory +// name. The PathOps are applied before returning the Dir. +// +// When used with Go 1.14+ the directory will be automatically removed when the test +// ends, unless the TEST_NOCLEANUP env var is set to true. +func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + path, err := ioutil.TempDir("", cleanPrefix(prefix)+"-") + assert.NilError(t, err) + dir := &Dir{path: path} + cleanup.Cleanup(t, dir.Remove) + + assert.NilError(t, applyPathOps(dir, ops)) + return dir +} + +// Path returns the full path to the directory +func (d *Dir) Path() string { + return d.path +} + +// Remove the directory +func (d *Dir) Remove() { + // nolint: errcheck + os.RemoveAll(d.path) +} + +// Join returns a new path with this directory as the base of the path +func (d *Dir) Join(parts ...string) string { + return filepath.Join(append([]string{d.Path()}, parts...)...) +} + +// DirFromPath returns a Dir for a path that already exists. No directory is created. +// Unlike NewDir the directory will not be removed automatically when the test exits, +// it is the callers responsibly to remove the directory. +// DirFromPath can be used with Apply to modify an existing directory. +// +// If the path does not already exist, use NewDir instead. +func DirFromPath(t assert.TestingT, path string, ops ...PathOp) *Dir { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + + dir := &Dir{path: path} + assert.NilError(t, applyPathOps(dir, ops)) + return dir +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/file_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/file_test.go new file mode 100644 index 0000000..5e8be4d --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/file_test.go @@ -0,0 +1,118 @@ +package fs_test + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" + "gotest.tools/v3/internal/source" + "gotest.tools/v3/skip" +) + +func TestNewDirWithOpsAndManifestEqual(t *testing.T) { + var userOps []fs.PathOp + if os.Geteuid() == 0 { + userOps = append(userOps, fs.AsUser(1001, 1002)) + } + + ops := []fs.PathOp{ + fs.WithFile("file1", "contenta", fs.WithMode(0400)), + fs.WithFile("file2", "", fs.WithBytes([]byte{0, 1, 2})), + fs.WithFile("file5", "", userOps...), + fs.WithSymlink("link1", "file1"), + fs.WithDir("sub", + fs.WithFiles(map[string]string{ + "file3": "contentb", + "file4": "contentc", + }), + fs.WithMode(0705), + ), + } + + dir := fs.NewDir(t, "test-all", ops...) + defer dir.Remove() + + manifestOps := append( + ops[:3], + fs.WithSymlink("link1", dir.Join("file1")), + ops[4], + ) + assert.Assert(t, fs.Equal(dir.Path(), fs.Expected(t, manifestOps...))) +} + +func TestNewFile(t *testing.T) { + t.Run("with test name", func(t *testing.T) { + tmpFile := fs.NewFile(t, t.Name()) + _, err := os.Stat(tmpFile.Path()) + assert.NilError(t, err) + + tmpFile.Remove() + _, err = os.Stat(tmpFile.Path()) + assert.ErrorType(t, err, os.IsNotExist) + }) + + t.Run(`with \ in name`, func(t *testing.T) { + tmpFile := fs.NewFile(t, `foo\thing`) + _, err := os.Stat(tmpFile.Path()) + assert.NilError(t, err) + + tmpFile.Remove() + _, err = os.Stat(tmpFile.Path()) + assert.ErrorType(t, err, os.IsNotExist) + }) +} + +func TestNewFile_IntegrationWithCleanup(t *testing.T) { + skip.If(t, source.GoVersionLessThan(1, 14)) + var tmpFile *fs.File + t.Run("cleanup in subtest", func(t *testing.T) { + tmpFile = fs.NewFile(t, t.Name()) + _, err := os.Stat(tmpFile.Path()) + assert.NilError(t, err) + }) + + t.Run("file has been removed", func(t *testing.T) { + _, err := os.Stat(tmpFile.Path()) + assert.ErrorType(t, err, os.IsNotExist) + }) +} + +func TestNewDir_IntegrationWithCleanup(t *testing.T) { + skip.If(t, source.GoVersionLessThan(1, 14)) + var tmpFile *fs.Dir + t.Run("cleanup in subtest", func(t *testing.T) { + tmpFile = fs.NewDir(t, t.Name()) + _, err := os.Stat(tmpFile.Path()) + assert.NilError(t, err) + }) + + t.Run("dir has been removed", func(t *testing.T) { + _, err := os.Stat(tmpFile.Path()) + assert.ErrorType(t, err, os.IsNotExist) + }) +} + +func TestDirFromPath(t *testing.T) { + tmpdir, err := ioutil.TempDir("", t.Name()) + assert.NilError(t, err) + t.Cleanup(func() { + os.RemoveAll(tmpdir) + }) + + dir := fs.DirFromPath(t, tmpdir, fs.WithFile("newfile", "")) + + _, err = os.Stat(dir.Join("newfile")) + assert.NilError(t, err) + + assert.Equal(t, dir.Path(), tmpdir) + assert.Equal(t, dir.Join("newfile"), filepath.Join(tmpdir, "newfile")) + + dir.Remove() + + _, err = os.Stat(tmpdir) + assert.Assert(t, errors.Is(err, os.ErrNotExist)) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest.go new file mode 100644 index 0000000..b657bd9 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest.go @@ -0,0 +1,139 @@ +package fs + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "gotest.tools/v3/assert" +) + +// Manifest stores the expected structure and properties of files and directories +// in a filesystem. +type Manifest struct { + root *directory +} + +type resource struct { + mode os.FileMode + uid uint32 + gid uint32 +} + +type file struct { + resource + content io.ReadCloser + ignoreCariageReturn bool + compareContentFunc func(b []byte) CompareResult +} + +func (f *file) Type() string { + return "file" +} + +type symlink struct { + resource + target string +} + +func (f *symlink) Type() string { + return "symlink" +} + +type directory struct { + resource + items map[string]dirEntry + filepathGlobs map[string]*filePath +} + +func (f *directory) Type() string { + return "directory" +} + +type dirEntry interface { + Type() string +} + +// ManifestFromDir creates a Manifest by reading the directory at path. The +// manifest stores the structure and properties of files in the directory. +// ManifestFromDir can be used with Equal to compare two directories. +func ManifestFromDir(t assert.TestingT, path string) Manifest { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + + manifest, err := manifestFromDir(path) + assert.NilError(t, err) + return manifest +} + +func manifestFromDir(path string) (Manifest, error) { + info, err := os.Stat(path) + switch { + case err != nil: + return Manifest{}, err + case !info.IsDir(): + return Manifest{}, fmt.Errorf("path %s must be a directory", path) + } + + directory, err := newDirectory(path, info) + return Manifest{root: directory}, err +} + +func newDirectory(path string, info os.FileInfo) (*directory, error) { + items := make(map[string]dirEntry) + children, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + for _, child := range children { + fullPath := filepath.Join(path, child.Name()) + items[child.Name()], err = getTypedResource(fullPath, child) + if err != nil { + return nil, err + } + } + + return &directory{ + resource: newResourceFromInfo(info), + items: items, + filepathGlobs: make(map[string]*filePath), + }, nil +} + +func getTypedResource(path string, info os.FileInfo) (dirEntry, error) { + switch { + case info.IsDir(): + return newDirectory(path, info) + case info.Mode()&os.ModeSymlink != 0: + return newSymlink(path, info) + // TODO: devices, pipes? + default: + return newFile(path, info) + } +} + +func newSymlink(path string, info os.FileInfo) (*symlink, error) { + target, err := os.Readlink(path) + if err != nil { + return nil, err + } + return &symlink{ + resource: newResourceFromInfo(info), + target: target, + }, err +} + +func newFile(path string, info os.FileInfo) (*file, error) { + // TODO: defer file opening to reduce number of open FDs? + readCloser, err := os.Open(path) + if err != nil { + return nil, err + } + return &file{ + resource: newResourceFromInfo(info), + content: readCloser, + }, err +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_test.go new file mode 100644 index 0000000..2cbc610 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_test.go @@ -0,0 +1,110 @@ +package fs + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "runtime" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert" +) + +func TestManifestFromDir(t *testing.T) { + var defaultFileMode os.FileMode = 0644 + var subDirMode = 0755 | os.ModeDir + var jFileMode os.FileMode = 0600 + if runtime.GOOS == "windows" { + defaultFileMode = 0666 + subDirMode = 0777 | os.ModeDir + jFileMode = 0666 + } + + var userOps []PathOp + var expectedUserResource = newResource(defaultFileMode) + if os.Geteuid() == 0 { + userOps = append(userOps, AsUser(1001, 1002)) + expectedUserResource = resource{mode: defaultFileMode, uid: 1001, gid: 1002} + } + + srcDir := NewDir(t, t.Name(), + WithFile("j", "content j", WithMode(0600)), + WithDir("s", + WithFile("k", "content k")), + WithSymlink("f", "j"), + WithFile("x", "content x", userOps...)) + defer srcDir.Remove() + + expected := Manifest{ + root: &directory{ + resource: newResource(defaultRootDirMode), + items: map[string]dirEntry{ + "j": &file{ + resource: newResource(jFileMode), + content: readCloser("content j"), + }, + "s": &directory{ + resource: newResource(subDirMode), + items: map[string]dirEntry{ + "k": &file{ + resource: newResource(defaultFileMode), + content: readCloser("content k"), + }, + }, + filepathGlobs: map[string]*filePath{}, + }, + "f": &symlink{ + resource: newResource(defaultSymlinkMode), + target: srcDir.Join("j"), + }, + "x": &file{ + resource: expectedUserResource, + content: readCloser("content x"), + }, + }, + filepathGlobs: map[string]*filePath{}, + }, + } + actual := ManifestFromDir(t, srcDir.Path()) + assert.DeepEqual(t, actual, expected, cmpManifest) + actual.root.items["j"].(*file).content.Close() + actual.root.items["x"].(*file).content.Close() + actual.root.items["s"].(*directory).items["k"].(*file).content.Close() +} + +func TestSymlinks(t *testing.T) { + rootDirectory := NewDir(t, "root", + WithFile("foo.txt", "foo"), + WithSymlink("foo.link", "foo.txt")) + defer rootDirectory.Remove() + expected := Expected(t, + WithFile("foo.txt", "foo"), + WithSymlink("foo.link", rootDirectory.Join("foo.txt"))) + assert.Assert(t, Equal(rootDirectory.Path(), expected)) +} + +var cmpManifest = cmp.Options{ + cmp.AllowUnexported(Manifest{}, resource{}, file{}, symlink{}, directory{}), + cmp.Comparer(func(x, y io.ReadCloser) bool { + if x == nil || y == nil { + return x == y + } + xContent, err := ioutil.ReadAll(x) + if err != nil { + return false + } + + yContent, err := ioutil.ReadAll(y) + if err != nil { + return false + } + return bytes.Equal(xContent, yContent) + }), +} + +func readCloser(s string) io.ReadCloser { + return ioutil.NopCloser(strings.NewReader(s)) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_unix.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_unix.go new file mode 100644 index 0000000..d2956f3 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_unix.go @@ -0,0 +1,38 @@ +//go:build !windows +// +build !windows + +package fs + +import ( + "os" + "runtime" + "syscall" +) + +const defaultRootDirMode = os.ModeDir | 0700 + +var defaultSymlinkMode = os.ModeSymlink | 0777 + +func init() { + switch runtime.GOOS { + case "darwin": + defaultSymlinkMode = os.ModeSymlink | 0755 + } +} + +func newResourceFromInfo(info os.FileInfo) resource { + statT := info.Sys().(*syscall.Stat_t) + return resource{ + mode: info.Mode(), + uid: statT.Uid, + gid: statT.Gid, + } +} + +func (p *filePath) SetMode(mode os.FileMode) { + p.file.mode = mode +} + +func (p *directoryPath) SetMode(mode os.FileMode) { + p.directory.mode = mode | os.ModeDir +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_windows.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_windows.go new file mode 100644 index 0000000..1c1a093 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/manifest_windows.go @@ -0,0 +1,22 @@ +package fs + +import "os" + +const ( + defaultRootDirMode = os.ModeDir | 0777 + defaultSymlinkMode = os.ModeSymlink | 0666 +) + +func newResourceFromInfo(info os.FileInfo) resource { + return resource{mode: info.Mode()} +} + +func (p *filePath) SetMode(mode os.FileMode) { + bits := mode & 0600 + p.file.mode = bits + bits/010 + bits/0100 +} + +// TODO: is mode ignored on windows? +func (p *directoryPath) SetMode(mode os.FileMode) { + p.directory.mode = defaultRootDirMode +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/ops.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/ops.go new file mode 100644 index 0000000..9e1e068 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/ops.go @@ -0,0 +1,275 @@ +package fs + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "gotest.tools/v3/assert" +) + +const defaultFileMode = 0644 + +// PathOp is a function which accepts a Path and performs an operation on that +// path. When called with real filesystem objects (File or Dir) a PathOp modifies +// the filesystem at the path. When used with a Manifest object a PathOp updates +// the manifest to expect a value. +type PathOp func(path Path) error + +type manifestResource interface { + SetMode(mode os.FileMode) + SetUID(uid uint32) + SetGID(gid uint32) +} + +type manifestFile interface { + manifestResource + SetContent(content io.ReadCloser) +} + +type manifestDirectory interface { + manifestResource + AddSymlink(path, target string) error + AddFile(path string, ops ...PathOp) error + AddDirectory(path string, ops ...PathOp) error +} + +// WithContent writes content to a file at Path +func WithContent(content string) PathOp { + return func(path Path) error { + if m, ok := path.(manifestFile); ok { + m.SetContent(ioutil.NopCloser(strings.NewReader(content))) + return nil + } + return ioutil.WriteFile(path.Path(), []byte(content), defaultFileMode) + } +} + +// WithBytes write bytes to a file at Path +func WithBytes(raw []byte) PathOp { + return func(path Path) error { + if m, ok := path.(manifestFile); ok { + m.SetContent(ioutil.NopCloser(bytes.NewReader(raw))) + return nil + } + return ioutil.WriteFile(path.Path(), raw, defaultFileMode) + } +} + +// WithReaderContent copies the reader contents to the file at Path +func WithReaderContent(r io.Reader) PathOp { + return func(path Path) error { + if m, ok := path.(manifestFile); ok { + m.SetContent(ioutil.NopCloser(r)) + return nil + } + f, err := os.OpenFile(path.Path(), os.O_WRONLY, defaultFileMode) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, r) + return err + } +} + +// AsUser changes ownership of the file system object at Path +func AsUser(uid, gid int) PathOp { + return func(path Path) error { + if m, ok := path.(manifestResource); ok { + m.SetUID(uint32(uid)) + m.SetGID(uint32(gid)) + return nil + } + return os.Chown(path.Path(), uid, gid) + } +} + +// WithFile creates a file in the directory at path with content +func WithFile(filename, content string, ops ...PathOp) PathOp { + return func(path Path) error { + if m, ok := path.(manifestDirectory); ok { + ops = append([]PathOp{WithContent(content), WithMode(defaultFileMode)}, ops...) + return m.AddFile(filename, ops...) + } + + fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename)) + if err := createFile(fullpath, content); err != nil { + return err + } + return applyPathOps(&File{path: fullpath}, ops) + } +} + +func createFile(fullpath string, content string) error { + return ioutil.WriteFile(fullpath, []byte(content), defaultFileMode) +} + +// WithFiles creates all the files in the directory at path with their content +func WithFiles(files map[string]string) PathOp { + return func(path Path) error { + if m, ok := path.(manifestDirectory); ok { + for filename, content := range files { + // TODO: remove duplication with WithFile + if err := m.AddFile(filename, WithContent(content), WithMode(defaultFileMode)); err != nil { + return err + } + } + return nil + } + + for filename, content := range files { + fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename)) + if err := createFile(fullpath, content); err != nil { + return err + } + } + return nil + } +} + +// FromDir copies the directory tree from the source path into the new Dir +func FromDir(source string) PathOp { + return func(path Path) error { + if _, ok := path.(manifestDirectory); ok { + return fmt.Errorf("use manifest.FromDir") + } + return copyDirectory(source, path.Path()) + } +} + +// WithDir creates a subdirectory in the directory at path. Additional PathOp +// can be used to modify the subdirectory +func WithDir(name string, ops ...PathOp) PathOp { + const defaultMode = 0755 + return func(path Path) error { + if m, ok := path.(manifestDirectory); ok { + ops = append([]PathOp{WithMode(defaultMode)}, ops...) + return m.AddDirectory(name, ops...) + } + + fullpath := filepath.Join(path.Path(), filepath.FromSlash(name)) + err := os.MkdirAll(fullpath, defaultMode) + if err != nil { + return err + } + return applyPathOps(&Dir{path: fullpath}, ops) + } +} + +// Apply the PathOps to the File +func Apply(t assert.TestingT, path Path, ops ...PathOp) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.NilError(t, applyPathOps(path, ops)) +} + +func applyPathOps(path Path, ops []PathOp) error { + for _, op := range ops { + if err := op(path); err != nil { + return err + } + } + return nil +} + +// WithMode sets the file mode on the directory or file at path +func WithMode(mode os.FileMode) PathOp { + return func(path Path) error { + if m, ok := path.(manifestResource); ok { + m.SetMode(mode) + return nil + } + return os.Chmod(path.Path(), mode) + } +} + +func copyDirectory(source, dest string) error { + entries, err := ioutil.ReadDir(source) + if err != nil { + return err + } + for _, entry := range entries { + sourcePath := filepath.Join(source, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + switch { + case entry.IsDir(): + if err := os.Mkdir(destPath, 0755); err != nil { + return err + } + if err := copyDirectory(sourcePath, destPath); err != nil { + return err + } + case entry.Mode()&os.ModeSymlink != 0: + if err := copySymLink(sourcePath, destPath); err != nil { + return err + } + default: + if err := copyFile(sourcePath, destPath); err != nil { + return err + } + } + } + return nil +} + +func copySymLink(source, dest string) error { + link, err := os.Readlink(source) + if err != nil { + return err + } + return os.Symlink(link, dest) +} + +func copyFile(source, dest string) error { + content, err := ioutil.ReadFile(source) + if err != nil { + return err + } + return ioutil.WriteFile(dest, content, 0644) +} + +// WithSymlink creates a symlink in the directory which links to target. +// Target must be a path relative to the directory. +// +// Note: the argument order is the inverse of os.Symlink to be consistent with +// the other functions in this package. +func WithSymlink(path, target string) PathOp { + return func(root Path) error { + if v, ok := root.(manifestDirectory); ok { + return v.AddSymlink(path, target) + } + return os.Symlink(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path)) + } +} + +// WithHardlink creates a link in the directory which links to target. +// Target must be a path relative to the directory. +// +// Note: the argument order is the inverse of os.Link to be consistent with +// the other functions in this package. +func WithHardlink(path, target string) PathOp { + return func(root Path) error { + if _, ok := root.(manifestDirectory); ok { + return fmt.Errorf("WithHardlink not implemented for manifests") + } + return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path)) + } +} + +// WithTimestamps sets the access and modification times of the file system object +// at path. +func WithTimestamps(atime, mtime time.Time) PathOp { + return func(root Path) error { + if _, ok := root.(manifestDirectory); ok { + return fmt.Errorf("WithTimestamp not implemented for manifests") + } + return os.Chtimes(root.Path(), atime, mtime) + } +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/ops_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/ops_test.go new file mode 100644 index 0000000..7185f2a --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/ops_test.go @@ -0,0 +1,100 @@ +package fs_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" +) + +func TestFromDir(t *testing.T) { + dir := fs.NewDir(t, "test-from-dir", fs.FromDir("testdata/copy-test")) + defer dir.Remove() + + expected := fs.Expected(t, + fs.WithFile("1", "1\n"), + fs.WithDir("a", + fs.WithFile("1", "1\n"), + fs.WithFile("2", "2\n"), + fs.WithDir("b", + fs.WithFile("1", "1\n")))) + + assert.Assert(t, fs.Equal(dir.Path(), expected)) +} + +func TestFromDirSymlink(t *testing.T) { + dir := fs.NewDir(t, "test-from-dir", fs.FromDir("testdata/copy-test-with-symlink")) + defer dir.Remove() + + currentdir, err := os.Getwd() + assert.NilError(t, err) + + link2 := filepath.FromSlash("../2") + link3 := "/some/inexistent/link" + if runtime.GOOS == "windows" { + link3 = filepath.Join(filepath.VolumeName(currentdir), link3) + } + + expected := fs.Expected(t, + fs.WithFile("1", "1\n"), + fs.WithDir("a", + fs.WithFile("1", "1\n"), + fs.WithFile("2", "2\n"), + fs.WithDir("b", + fs.WithFile("1", "1\n"), + fs.WithSymlink("2", link2), + fs.WithSymlink("3", link3), + fs.WithSymlink("4", "5"), + ))) + + assert.Assert(t, fs.Equal(dir.Path(), expected)) +} + +func TestWithTimestamps(t *testing.T) { + stamp := time.Date(2011, 11, 11, 5, 55, 55, 0, time.UTC) + tmpFile := fs.NewFile(t, t.Name(), fs.WithTimestamps(stamp, stamp)) + defer tmpFile.Remove() + + stat, err := os.Stat(tmpFile.Path()) + assert.NilError(t, err) + assert.DeepEqual(t, stat.ModTime(), stamp) +} + +func TestApply(t *testing.T) { + t.Run("with file", func(t *testing.T) { + tmpFile := fs.NewFile(t, "test-update-file", fs.WithContent("contenta")) + defer tmpFile.Remove() + fs.Apply(t, tmpFile, fs.WithContent("contentb")) + content, err := ioutil.ReadFile(tmpFile.Path()) + assert.NilError(t, err) + assert.Equal(t, string(content), "contentb") + }) + + t.Run("with dir", func(t *testing.T) { + tmpDir := fs.NewDir(t, "test-update-dir") + defer tmpDir.Remove() + fs.Apply(t, tmpDir, fs.WithFile("file1", "contenta")) + fs.Apply(t, tmpDir, fs.WithFile("file2", "contentb")) + expected := fs.Expected(t, + fs.WithFile("file1", "contenta"), + fs.WithFile("file2", "contentb")) + assert.Assert(t, fs.Equal(tmpDir.Path(), expected)) + }) +} + +func TestWithReaderContent(t *testing.T) { + content := "this is a test" + dir := fs.NewDir(t, t.Name(), + fs.WithFile("1", "", + fs.WithReaderContent(strings.NewReader(content))), + ) + defer dir.Remove() + expected := fs.Expected(t, fs.WithFile("1", content)) + assert.Assert(t, fs.Equal(dir.Path(), expected)) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/path.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/path.go new file mode 100644 index 0000000..c301b90 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/path.go @@ -0,0 +1,199 @@ +package fs + +import ( + "bytes" + "io" + "io/ioutil" + "os" + + "gotest.tools/v3/assert" +) + +// resourcePath is an adaptor for resources so they can be used as a Path +// with PathOps. +type resourcePath struct{} + +func (p *resourcePath) Path() string { + return "manifest: not a filesystem path" +} + +func (p *resourcePath) Remove() {} + +type filePath struct { + resourcePath + file *file +} + +func (p *filePath) SetContent(content io.ReadCloser) { + p.file.content = content +} + +func (p *filePath) SetUID(uid uint32) { + p.file.uid = uid +} + +func (p *filePath) SetGID(gid uint32) { + p.file.gid = gid +} + +type directoryPath struct { + resourcePath + directory *directory +} + +func (p *directoryPath) SetUID(uid uint32) { + p.directory.uid = uid +} + +func (p *directoryPath) SetGID(gid uint32) { + p.directory.gid = gid +} + +func (p *directoryPath) AddSymlink(path, target string) error { + p.directory.items[path] = &symlink{ + resource: newResource(defaultSymlinkMode), + target: target, + } + return nil +} + +func (p *directoryPath) AddFile(path string, ops ...PathOp) error { + newFile := &file{resource: newResource(0)} + p.directory.items[path] = newFile + exp := &filePath{file: newFile} + return applyPathOps(exp, ops) +} + +func (p *directoryPath) AddGlobFiles(glob string, ops ...PathOp) error { + newFile := &file{resource: newResource(0)} + newFilePath := &filePath{file: newFile} + p.directory.filepathGlobs[glob] = newFilePath + return applyPathOps(newFilePath, ops) +} + +func (p *directoryPath) AddDirectory(path string, ops ...PathOp) error { + newDir := newDirectoryWithDefaults() + p.directory.items[path] = newDir + exp := &directoryPath{directory: newDir} + return applyPathOps(exp, ops) +} + +// Expected returns a Manifest with a directory structured created by ops. The +// PathOp operations are applied to the manifest as expectations of the +// filesystem structure and properties. +func Expected(t assert.TestingT, ops ...PathOp) Manifest { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + + newDir := newDirectoryWithDefaults() + e := &directoryPath{directory: newDir} + assert.NilError(t, applyPathOps(e, ops)) + return Manifest{root: newDir} +} + +func newDirectoryWithDefaults() *directory { + return &directory{ + resource: newResource(defaultRootDirMode), + items: make(map[string]dirEntry), + filepathGlobs: make(map[string]*filePath), + } +} + +func newResource(mode os.FileMode) resource { + return resource{ + mode: mode, + uid: currentUID(), + gid: currentGID(), + } +} + +func currentUID() uint32 { + return normalizeID(os.Getuid()) +} + +func currentGID() uint32 { + return normalizeID(os.Getgid()) +} + +func normalizeID(id int) uint32 { + // ids will be -1 on windows + if id < 0 { + return 0 + } + return uint32(id) +} + +var anyFileContent = ioutil.NopCloser(bytes.NewReader(nil)) + +// MatchAnyFileContent is a PathOp that updates a Manifest so that the file +// at path may contain any content. +func MatchAnyFileContent(path Path) error { + if m, ok := path.(*filePath); ok { + m.SetContent(anyFileContent) + } + return nil +} + +// MatchContentIgnoreCarriageReturn is a PathOp that ignores cariage return +// discrepancies. +func MatchContentIgnoreCarriageReturn(path Path) error { + if m, ok := path.(*filePath); ok { + m.file.ignoreCariageReturn = true + } + return nil +} + +const anyFile = "*" + +// MatchExtraFiles is a PathOp that updates a Manifest to allow a directory +// to contain unspecified files. +func MatchExtraFiles(path Path) error { + if m, ok := path.(*directoryPath); ok { + return m.AddFile(anyFile) + } + return nil +} + +// CompareResult is the result of comparison. +// +// See gotest.tools/assert/cmp.StringResult for a convenient implementation of +// this interface. +type CompareResult interface { + Success() bool + FailureMessage() string +} + +// MatchFileContent is a PathOp that updates a Manifest to use the provided +// function to determine if a file's content matches the expectation. +func MatchFileContent(f func([]byte) CompareResult) PathOp { + return func(path Path) error { + if m, ok := path.(*filePath); ok { + m.file.compareContentFunc = f + } + return nil + } +} + +// MatchFilesWithGlob is a PathOp that updates a Manifest to match files using +// glob pattern, and check them using the ops. +func MatchFilesWithGlob(glob string, ops ...PathOp) PathOp { + return func(path Path) error { + if m, ok := path.(*directoryPath); ok { + return m.AddGlobFiles(glob, ops...) + } + return nil + } +} + +// anyFileMode is represented by uint32_max +const anyFileMode os.FileMode = 4294967295 + +// MatchAnyFileMode is a PathOp that updates a Manifest so that the resource at path +// will match any file mode. +func MatchAnyFileMode(path Path) error { + if m, ok := path.(manifestResource); ok { + m.SetMode(anyFileMode) + } + return nil +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/report.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/report.go new file mode 100644 index 0000000..1a3c668 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/report.go @@ -0,0 +1,276 @@ +package fs + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/format" +) + +// Equal compares a directory to the expected structured described by a manifest +// and returns success if they match. If they do not match the failure message +// will contain all the differences between the directory structure and the +// expected structure defined by the Manifest. +// +// Equal is a cmp.Comparison which can be used with assert.Assert(). +func Equal(path string, expected Manifest) cmp.Comparison { + return func() cmp.Result { + actual, err := manifestFromDir(path) + if err != nil { + return cmp.ResultFromError(err) + } + failures := eqDirectory(string(os.PathSeparator), expected.root, actual.root) + if len(failures) == 0 { + return cmp.ResultSuccess + } + msg := fmt.Sprintf("directory %s does not match expected:\n", path) + return cmp.ResultFailure(msg + formatFailures(failures)) + } +} + +type failure struct { + path string + problems []problem +} + +type problem string + +func notEqual(property string, x, y interface{}) problem { + return problem(fmt.Sprintf("%s: expected %s got %s", property, x, y)) +} + +func errProblem(reason string, err error) problem { + return problem(fmt.Sprintf("%s: %s", reason, err)) +} + +func existenceProblem(filename, reason string, args ...interface{}) problem { + return problem(filename + ": " + fmt.Sprintf(reason, args...)) +} + +func eqResource(x, y resource) []problem { + var p []problem + if x.uid != y.uid { + p = append(p, notEqual("uid", x.uid, y.uid)) + } + if x.gid != y.gid { + p = append(p, notEqual("gid", x.gid, y.gid)) + } + if x.mode != anyFileMode && x.mode != y.mode { + p = append(p, notEqual("mode", x.mode, y.mode)) + } + return p +} + +func removeCarriageReturn(in []byte) []byte { + return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1) +} + +func eqFile(x, y *file) []problem { + p := eqResource(x.resource, y.resource) + + switch { + case x.content == nil: + p = append(p, existenceProblem("content", "expected content is nil")) + return p + case x.content == anyFileContent: + return p + case y.content == nil: + p = append(p, existenceProblem("content", "actual content is nil")) + return p + } + + xContent, xErr := ioutil.ReadAll(x.content) + defer x.content.Close() + yContent, yErr := ioutil.ReadAll(y.content) + defer y.content.Close() + + if xErr != nil { + p = append(p, errProblem("failed to read expected content", xErr)) + } + if yErr != nil { + p = append(p, errProblem("failed to read actual content", xErr)) + } + if xErr != nil || yErr != nil { + return p + } + + if x.compareContentFunc != nil { + r := x.compareContentFunc(yContent) + if !r.Success() { + p = append(p, existenceProblem("content", r.FailureMessage())) + } + return p + } + + if x.ignoreCariageReturn || y.ignoreCariageReturn { + xContent = removeCarriageReturn(xContent) + yContent = removeCarriageReturn(yContent) + } + + if !bytes.Equal(xContent, yContent) { + p = append(p, diffContent(xContent, yContent)) + } + return p +} + +func diffContent(x, y []byte) problem { + diff := format.UnifiedDiff(format.DiffConfig{ + A: string(x), + B: string(y), + From: "expected", + To: "actual", + }) + // Remove the trailing newline in the diff. A trailing newline is always + // added to a problem by formatFailures. + diff = strings.TrimSuffix(diff, "\n") + return problem("content:\n" + indent(diff, " ")) +} + +func indent(s, prefix string) string { + buf := new(bytes.Buffer) + lines := strings.SplitAfter(s, "\n") + for _, line := range lines { + buf.WriteString(prefix + line) + } + return buf.String() +} + +func eqSymlink(x, y *symlink) []problem { + p := eqResource(x.resource, y.resource) + xTarget := x.target + yTarget := y.target + if runtime.GOOS == "windows" { + xTarget = strings.ToLower(xTarget) + yTarget = strings.ToLower(yTarget) + } + if xTarget != yTarget { + p = append(p, notEqual("target", x.target, y.target)) + } + return p +} + +func eqDirectory(path string, x, y *directory) []failure { + p := eqResource(x.resource, y.resource) + var f []failure + matchedFiles := make(map[string]bool) + + for _, name := range sortedKeys(x.items) { + if name == anyFile { + continue + } + matchedFiles[name] = true + xEntry := x.items[name] + yEntry, ok := y.items[name] + if !ok { + p = append(p, existenceProblem(name, "expected %s to exist", xEntry.Type())) + continue + } + + if xEntry.Type() != yEntry.Type() { + p = append(p, notEqual(name, xEntry.Type(), yEntry.Type())) + continue + } + + f = append(f, eqEntry(filepath.Join(path, name), xEntry, yEntry)...) + } + + if len(x.filepathGlobs) != 0 { + for _, name := range sortedKeys(y.items) { + m := matchGlob(name, y.items[name], x.filepathGlobs) + matchedFiles[name] = m.match + f = append(f, m.failures...) + } + } + + if _, ok := x.items[anyFile]; ok { + return maybeAppendFailure(f, path, p) + } + for _, name := range sortedKeys(y.items) { + if !matchedFiles[name] { + p = append(p, existenceProblem(name, "unexpected %s", y.items[name].Type())) + } + } + return maybeAppendFailure(f, path, p) +} + +func maybeAppendFailure(failures []failure, path string, problems []problem) []failure { + if len(problems) > 0 { + return append(failures, failure{path: path, problems: problems}) + } + return failures +} + +func sortedKeys(items map[string]dirEntry) []string { + keys := make([]string, 0, len(items)) + for key := range items { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +// eqEntry assumes x and y to be the same type +func eqEntry(path string, x, y dirEntry) []failure { + resp := func(problems []problem) []failure { + if len(problems) == 0 { + return nil + } + return []failure{{path: path, problems: problems}} + } + + switch typed := x.(type) { + case *file: + return resp(eqFile(typed, y.(*file))) + case *symlink: + return resp(eqSymlink(typed, y.(*symlink))) + case *directory: + return eqDirectory(path, typed, y.(*directory)) + } + return nil +} + +type globMatch struct { + match bool + failures []failure +} + +func matchGlob(name string, yEntry dirEntry, globs map[string]*filePath) globMatch { + m := globMatch{} + + for glob, expectedFile := range globs { + ok, err := filepath.Match(glob, name) + if err != nil { + p := errProblem("failed to match glob pattern", err) + f := failure{path: name, problems: []problem{p}} + m.failures = append(m.failures, f) + } + if ok { + m.match = true + m.failures = eqEntry(name, expectedFile.file, yEntry) + return m + } + } + return m +} + +func formatFailures(failures []failure) string { + sort.Slice(failures, func(i, j int) bool { + return failures[i].path < failures[j].path + }) + + buf := new(bytes.Buffer) + for _, failure := range failures { + buf.WriteString(failure.path + "\n") + for _, problem := range failure.problems { + buf.WriteString(" " + string(problem) + "\n") + } + } + return buf.String() +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/report_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/report_test.go new file mode 100644 index 0000000..389d608 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/report_test.go @@ -0,0 +1,273 @@ +package fs + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/skip" +) + +func TestEqualMissingRoot(t *testing.T) { + result := Equal("/bogus/path/does/not/exist", Expected(t))() + assert.Assert(t, !result.Success()) + expected := "stat /bogus/path/does/not/exist: no such file or directory" + if runtime.GOOS == "windows" { + expected = "CreateFile /bogus/path/does/not/exist" + } + assert.Assert(t, is.Contains(result.(cmpFailure).FailureMessage(), expected)) +} + +func TestEqualModeMismatch(t *testing.T) { + dir := NewDir(t, t.Name(), WithMode(0500)) + defer dir.Remove() + + result := Equal(dir.Path(), Expected(t))() + assert.Assert(t, !result.Success()) + expected := fmtExpected(`directory %s does not match expected: +/ + mode: expected drwx------ got dr-x------ +`, dir.Path()) + if runtime.GOOS == "windows" { + expected = fmtExpected(`directory %s does not match expected: +\ + mode: expected drwxrwxrwx got dr-xr-xr-x +`, dir.Path()) + } + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) +} + +func TestEqualRootIsAFile(t *testing.T) { + file := NewFile(t, t.Name()) + defer file.Remove() + + result := Equal(file.Path(), Expected(t))() + assert.Assert(t, !result.Success()) + expected := fmt.Sprintf("path %s must be a directory", file.Path()) + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) +} + +func TestEqualSuccess(t *testing.T) { + dir := NewDir(t, t.Name(), WithMode(0700)) + defer dir.Remove() + + assert.Assert(t, Equal(dir.Path(), Expected(t))) +} + +func TestEqualDirectoryHasWithExtraFiles(t *testing.T) { + dir := NewDir(t, t.Name(), + WithFile("extra1", "content")) + defer dir.Remove() + + manifest := Expected(t, WithFile("file1", "content")) + result := Equal(dir.Path(), manifest)() + assert.Assert(t, !result.Success()) + expected := fmtExpected(`directory %s does not match expected: +/ + file1: expected file to exist + extra1: unexpected file +`, dir.Path()) + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) +} + +func fmtExpected(format string, args ...interface{}) string { + return filepath.FromSlash(fmt.Sprintf(format, args...)) +} + +func TestEqualWithMatchAnyFileContent(t *testing.T) { + dir := NewDir(t, t.Name(), + WithFile("data", "this is some data")) + defer dir.Remove() + + expected := Expected(t, + WithFile("data", "different content", MatchAnyFileContent)) + assert.Assert(t, Equal(dir.Path(), expected)) +} + +func TestEqualWithFileContent(t *testing.T) { + dir := NewDir(t, "assert-test-root", + WithFile("file1", "line1\nline2\nline3")) + defer dir.Remove() + + manifest := Expected(t, + WithFile("file1", "line2\nline3")) + + result := Equal(dir.Path(), manifest)() + expected := fmtExpected(`directory %s does not match expected: +/file1 + content: + --- expected + +++ actual + @@ -1,2 +1,3 @@ + +line1 + line2 + line3 +`, dir.Path()) + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) +} + +func TestEqualWithMatchContentIgnoreCarriageReturn(t *testing.T) { + dir := NewDir(t, t.Name(), + WithFile("file1", "line1\r\nline2")) + defer dir.Remove() + + manifest := Expected(t, + WithFile("file1", "line1\nline2", MatchContentIgnoreCarriageReturn)) + + result := Equal(dir.Path(), manifest)() + assert.Assert(t, result.Success()) +} + +func TestEqualDirectoryWithMatchExtraFiles(t *testing.T) { + file1 := WithFile("file1", "same in both") + dir := NewDir(t, t.Name(), + file1, + WithFile("extra", "some content")) + defer dir.Remove() + + expected := Expected(t, file1, MatchExtraFiles) + assert.Assert(t, Equal(dir.Path(), expected)) +} + +func TestEqualManyFailures(t *testing.T) { + dir := NewDir(t, t.Name(), + WithFile("file1", "same in both"), + WithFile("extra", "some content"), + WithSymlink("sym1", "extra")) + defer dir.Remove() + + manifest := Expected(t, + WithDir("subdir", + WithFile("somefile", "")), + WithFile("file1", "not the\nsame in both")) + + result := Equal(dir.Path(), manifest)() + assert.Assert(t, !result.Success()) + + expected := fmtExpected(`directory %s does not match expected: +/ + subdir: expected directory to exist + extra: unexpected file + sym1: unexpected symlink +/file1 + content: + --- expected + +++ actual + @@ -1,2 +1 @@ + -not the + same in both +`, dir.Path()) + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) +} + +type cmpFailure interface { + FailureMessage() string +} + +func TestMatchAnyFileMode(t *testing.T) { + dir := NewDir(t, t.Name(), + WithFile("data", "content", + WithMode(0777))) + defer dir.Remove() + + expected := Expected(t, + WithFile("data", "content", MatchAnyFileMode)) + assert.Assert(t, Equal(dir.Path(), expected)) +} + +func TestMatchFileContent(t *testing.T) { + dir := NewDir(t, t.Name(), + WithFile("data", "content")) + defer dir.Remove() + + t.Run("content matches", func(t *testing.T) { + matcher := func(b []byte) CompareResult { + return is.ResultSuccess + } + manifest := Expected(t, + WithFile("data", "different", MatchFileContent(matcher))) + assert.Assert(t, Equal(dir.Path(), manifest)) + }) + + t.Run("content does not match", func(t *testing.T) { + matcher := func(b []byte) CompareResult { + return is.ResultFailure("data content differs from expected") + } + manifest := Expected(t, + WithFile("data", "content", MatchFileContent(matcher))) + result := Equal(dir.Path(), manifest)() + assert.Assert(t, !result.Success()) + + expected := fmtExpected(`directory %s does not match expected: +/data + content: data content differs from expected +`, dir.Path()) + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) + }) +} + +func TestMatchExtraFilesGlob(t *testing.T) { + dir := NewDir(t, t.Name(), + WithFile("t.go", "data"), + WithFile("a.go", "data"), + WithFile("conf.yml", "content", WithMode(0600))) + defer dir.Remove() + + t.Run("matching globs", func(t *testing.T) { + manifest := Expected(t, + MatchFilesWithGlob("*.go", MatchAnyFileMode, MatchAnyFileContent), + MatchFilesWithGlob("*.yml", MatchAnyFileMode, MatchAnyFileContent)) + assert.Assert(t, Equal(dir.Path(), manifest)) + }) + + t.Run("matching globs with wrong mode", func(t *testing.T) { + skip.If(t, runtime.GOOS == "windows", "expect mode does not match on windows") + manifest := Expected(t, + MatchFilesWithGlob("*.go", MatchAnyFileMode, MatchAnyFileContent), + MatchFilesWithGlob("*.yml", MatchAnyFileContent, WithMode(0700))) + + result := Equal(dir.Path(), manifest)() + + assert.Assert(t, !result.Success()) + expected := fmtExpected(`directory %s does not match expected: +conf.yml + mode: expected -rwx------ got -rw------- +`, dir.Path()) + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) + }) + + t.Run("matching partial glob", func(t *testing.T) { + manifest := Expected(t, MatchFilesWithGlob("*.go", MatchAnyFileMode, MatchAnyFileContent)) + result := Equal(dir.Path(), manifest)() + assert.Assert(t, !result.Success()) + + expected := fmtExpected(`directory %s does not match expected: +/ + conf.yml: unexpected file +`, dir.Path()) + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) + }) + + t.Run("invalid glob", func(t *testing.T) { + manifest := Expected(t, MatchFilesWithGlob("[-x]")) + result := Equal(dir.Path(), manifest)() + assert.Assert(t, !result.Success()) + + expected := fmtExpected(`directory %s does not match expected: +/ + a.go: unexpected file + conf.yml: unexpected file + t.go: unexpected file +a.go + failed to match glob pattern: syntax error in pattern +conf.yml + failed to match glob pattern: syntax error in pattern +t.go + failed to match glob pattern: syntax error in pattern +`, dir.Path()) + assert.Equal(t, result.(cmpFailure).FailureMessage(), expected) + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/1 b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/1 new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/1 @@ -0,0 +1 @@ +1 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/1 b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/1 new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/1 @@ -0,0 +1 @@ +1 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/2 b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/2 new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/2 @@ -0,0 +1 @@ +2 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/b/1 b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/b/1 new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test-with-symlink/a/b/1 @@ -0,0 +1 @@ +1 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/1 b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/1 new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/1 @@ -0,0 +1 @@ +1 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/1 b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/1 new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/1 @@ -0,0 +1 @@ +1 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/2 b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/2 new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/2 @@ -0,0 +1 @@ +2 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/b/1 b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/b/1 new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/fs/testdata/copy-test/a/b/1 @@ -0,0 +1 @@ +1 diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/go.mod b/root/pkg/mod/gotest.tools/v3@v3.3.0/go.mod new file mode 100644 index 0000000..abeca34 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/go.mod @@ -0,0 +1,10 @@ +module gotest.tools/v3 + +go 1.13 + +require ( + github.com/google/go-cmp v0.5.5 + github.com/spf13/pflag v1.0.3 + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 + golang.org/x/tools v0.1.0 +) diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/go.sum b/root/pkg/mod/gotest.tools/v3@v3.3.0/go.sum new file mode 100644 index 0000000..ab53950 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/go.sum @@ -0,0 +1,31 @@ +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/example_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/example_test.go new file mode 100644 index 0000000..7f9847e --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/example_test.go @@ -0,0 +1,22 @@ +package golden_test + +import ( + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/golden" +) + +var t = &testing.T{} + +func ExampleAssert() { + golden.Assert(t, "foo", "foo-content.golden") +} + +func ExampleString() { + assert.Assert(t, golden.String("foo", "foo-content.golden")) +} + +func ExampleAssertBytes() { + golden.AssertBytes(t, []byte("foo"), "foo-content.golden") +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/golden.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/golden.go new file mode 100644 index 0000000..47ea85f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/golden.go @@ -0,0 +1,190 @@ +/*Package golden provides tools for comparing large mutli-line strings. + +Golden files are files in the ./testdata/ subdirectory of the package under test. +Golden files can be automatically updated to match new values by running +`go test pkgname -update`. To ensure the update is correct +compare the diff of the old expected value to the new expected value. +*/ +package golden // import "gotest.tools/v3/golden" + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/format" + "gotest.tools/v3/internal/source" +) + +func init() { + flag.BoolVar(&source.Update, "test.update-golden", false, "deprecated flag") +} + +type helperT interface { + Helper() +} + +// NormalizeCRLFToLF enables end-of-line normalization for actual values passed +// to Assert and String, as well as the values saved to golden files with +// -update. +// +// Defaults to true. If you use the core.autocrlf=true git setting on windows +// you will need to set this to false. +// +// The value may be set to false by setting GOTESTTOOLS_GOLDEN_NormalizeCRLFToLF=false +// in the environment before running tests. +// +// The default value may change in a future major release. +var NormalizeCRLFToLF = os.Getenv("GOTESTTOOLS_GOLDEN_NormalizeCRLFToLF") != "false" + +// FlagUpdate returns true when the -update flag has been set. +func FlagUpdate() bool { + return source.Update +} + +// Open opens the file in ./testdata +func Open(t assert.TestingT, filename string) *os.File { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + f, err := os.Open(Path(filename)) + assert.NilError(t, err) + return f +} + +// Get returns the contents of the file in ./testdata +func Get(t assert.TestingT, filename string) []byte { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + expected, err := ioutil.ReadFile(Path(filename)) + assert.NilError(t, err) + return expected +} + +// Path returns the full path to a file in ./testdata +func Path(filename string) string { + if filepath.IsAbs(filename) { + return filename + } + return filepath.Join("testdata", filename) +} + +func removeCarriageReturn(in []byte) []byte { + if !NormalizeCRLFToLF { + return in + } + return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1) +} + +// Assert compares actual to the expected value in the golden file. +// +// Running `go test pkgname -update` will write the value of actual +// to the golden file. +// +// This is equivalent to assert.Assert(t, String(actual, filename)) +func Assert(t assert.TestingT, actual string, filename string, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.Assert(t, String(actual, filename), msgAndArgs...) +} + +// String compares actual to the contents of filename and returns success +// if the strings are equal. +// +// Running `go test pkgname -update` will write the value of actual +// to the golden file. +// +// Any \r\n substrings in actual are converted to a single \n character +// before comparing it to the expected string. When updating the golden file the +// normalized version will be written to the file. This allows Windows to use +// the same golden files as other operating systems. +func String(actual string, filename string) cmp.Comparison { + return func() cmp.Result { + actualBytes := removeCarriageReturn([]byte(actual)) + result, expected := compare(actualBytes, filename) + if result != nil { + return result + } + diff := format.UnifiedDiff(format.DiffConfig{ + A: string(expected), + B: string(actualBytes), + From: "expected", + To: "actual", + }) + return cmp.ResultFailure("\n" + diff + failurePostamble(filename)) + } +} + +func failurePostamble(filename string) string { + return fmt.Sprintf(` + +You can run 'go test . -update' to automatically update %s to the new expected value.' +`, Path(filename)) +} + +// AssertBytes compares actual to the expected value in the golden. +// +// Running `go test pkgname -update` will write the value of actual +// to the golden file. +// +// This is equivalent to assert.Assert(t, Bytes(actual, filename)) +func AssertBytes( + t assert.TestingT, + actual []byte, + filename string, + msgAndArgs ...interface{}, +) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.Assert(t, Bytes(actual, filename), msgAndArgs...) +} + +// Bytes compares actual to the contents of filename and returns success +// if the bytes are equal. +// +// Running `go test pkgname -update` will write the value of actual +// to the golden file. +func Bytes(actual []byte, filename string) cmp.Comparison { + return func() cmp.Result { + result, expected := compare(actual, filename) + if result != nil { + return result + } + msg := fmt.Sprintf("%v (actual) != %v (expected)", actual, expected) + return cmp.ResultFailure(msg + failurePostamble(filename)) + } +} + +func compare(actual []byte, filename string) (cmp.Result, []byte) { + if err := update(filename, actual); err != nil { + return cmp.ResultFromError(err), nil + } + expected, err := ioutil.ReadFile(Path(filename)) + if err != nil { + return cmp.ResultFromError(err), nil + } + if bytes.Equal(expected, actual) { + return cmp.ResultSuccess, nil + } + return nil, expected +} + +func update(filename string, actual []byte) error { + if !source.Update { + return nil + } + if dir := filepath.Dir(Path(filename)); dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + return ioutil.WriteFile(Path(filename), actual, 0644) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/golden_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/golden_test.go new file mode 100644 index 0000000..3b0bd02 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/golden/golden_test.go @@ -0,0 +1,296 @@ +package golden + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/fs" + "gotest.tools/v3/internal/source" +) + +type fakeT struct { + Failed bool +} + +func (t *fakeT) Log(...interface{}) { +} + +func (t *fakeT) FailNow() { + t.Failed = true +} + +func (t *fakeT) Fail() { + t.Failed = true +} + +func (t *fakeT) Helper() {} + +func TestGoldenOpenInvalidFile(t *testing.T) { + fakeT := new(fakeT) + + Open(fakeT, "/invalid/path") + assert.Assert(t, fakeT.Failed) +} + +func TestGoldenOpenAbsolutePath(t *testing.T) { + file := fs.NewFile(t, "abs-test", fs.WithContent("content\n")) + defer file.Remove() + fakeT := new(fakeT) + + f := Open(fakeT, file.Path()) + assert.Assert(t, !fakeT.Failed) + f.Close() +} + +func TestGoldenOpen(t *testing.T) { + filename, clean := setupGoldenFile(t, "") + defer clean() + + fakeT := new(fakeT) + + f := Open(fakeT, filename) + assert.Assert(t, !fakeT.Failed) + f.Close() +} + +func TestGoldenGetInvalidFile(t *testing.T) { + fakeT := new(fakeT) + + Get(fakeT, "/invalid/path") + assert.Assert(t, fakeT.Failed) +} + +func TestGoldenGetAbsolutePath(t *testing.T) { + file := fs.NewFile(t, "abs-test", fs.WithContent("content\n")) + defer file.Remove() + fakeT := new(fakeT) + + Get(fakeT, file.Path()) + assert.Assert(t, !fakeT.Failed) +} + +func TestGoldenGet(t *testing.T) { + expected := "content\nline1\nline2" + + filename, clean := setupGoldenFile(t, expected) + defer clean() + + fakeT := new(fakeT) + + actual := Get(fakeT, filename) + assert.Assert(t, !fakeT.Failed) + assert.Assert(t, cmp.DeepEqual(actual, []byte(expected))) +} + +func TestGoldenAssertInvalidContent(t *testing.T) { + filename, clean := setupGoldenFile(t, "content") + defer clean() + + fakeT := new(fakeT) + + Assert(fakeT, "foo", filename) + assert.Assert(t, fakeT.Failed) +} + +func TestGoldenAssertInvalidContentUpdate(t *testing.T) { + setUpdateFlag(t) + filename, clean := setupGoldenFile(t, "content") + defer clean() + + fakeT := new(fakeT) + + Assert(fakeT, "foo", filename) + assert.Assert(t, !fakeT.Failed) +} + +func TestGoldenAssertAbsolutePath(t *testing.T) { + file := fs.NewFile(t, "abs-test", fs.WithContent("foo")) + defer file.Remove() + fakeT := new(fakeT) + + Assert(fakeT, "foo", file.Path()) + assert.Assert(t, !fakeT.Failed) +} + +func TestGoldenAssertInDir(t *testing.T) { + filename, clean := setupGoldenFileWithDir(t, "testdatasubdir", "foo") + defer clean() + + fakeT := new(fakeT) + + Assert(fakeT, "foo", filepath.Join("testdatasubdir", filename)) + assert.Assert(t, !fakeT.Failed) + + _, err := os.Stat("testdatasubdir") + assert.Assert(t, os.IsNotExist(err), "testdatasubdir should not exist outside of testdata") +} + +func TestGoldenAssertInDir_UpdateGolden(t *testing.T) { + filename, clean := setupGoldenFileWithDir(t, "testdatasubdir", "foo") + defer clean() + setUpdateFlag(t) + + fakeT := new(fakeT) + + Assert(fakeT, "foo", filepath.Join("testdatasubdir", filename)) + assert.Assert(t, !fakeT.Failed) + + _, err := os.Stat("testdatasubdir") + assert.Assert(t, os.IsNotExist(err), "testdatasubdir should not exist outside of testdata") +} + +func TestGoldenAssert(t *testing.T) { + filename, clean := setupGoldenFile(t, "foo") + defer clean() + + fakeT := new(fakeT) + + Assert(fakeT, "foo", filename) + assert.Assert(t, !fakeT.Failed) +} + +func TestAssert_WithCarriageReturnInActual(t *testing.T) { + filename, clean := setupGoldenFile(t, "a\rfoo\nbar\n") + defer clean() + + fakeT := new(fakeT) + + Assert(fakeT, "a\rfoo\r\nbar\r\n", filename) + assert.Assert(t, !fakeT.Failed) +} + +func TestAssert_WithCarriageReturnInActual_UpdateGolden(t *testing.T) { + filename, clean := setupGoldenFile(t, "") + defer clean() + unsetUpdateFlag := setUpdateFlag(t) + + fakeT := new(fakeT) + Assert(fakeT, "a\rfoo\r\nbar\r\n", filename) + assert.Assert(t, !fakeT.Failed) + + unsetUpdateFlag() + actual := Get(fakeT, filename) + assert.Equal(t, string(actual), "a\rfoo\nbar\n") + + Assert(t, "a\rfoo\r\nbar\r\n", filename, "matches with carriage returns") + Assert(t, "a\rfoo\nbar\n", filename, "matches without carriage returns") +} + +func TestGoldenAssertBytes(t *testing.T) { + filename, clean := setupGoldenFile(t, "foo") + defer clean() + + fakeT := new(fakeT) + + AssertBytes(fakeT, []byte("foo"), filename) + assert.Assert(t, !fakeT.Failed) +} + +func setUpdateFlag(t *testing.T) func() { + orig := source.Update + source.Update = true + undo := func() { + source.Update = orig + } + t.Cleanup(undo) + return undo +} + +func setupGoldenFileWithDir(t *testing.T, dirname, content string) (string, func()) { + dirpath := filepath.Join("testdata", dirname) + _ = os.MkdirAll(filepath.Join("testdata", dirname), 0755) + f, err := ioutil.TempFile(dirpath, t.Name()+"-") + assert.NilError(t, err, "fail to create test golden file") + defer f.Close() + + _, err = f.Write([]byte(content)) + assert.NilError(t, err) + + return filepath.Base(f.Name()), func() { + assert.NilError(t, os.Remove(f.Name())) + assert.NilError(t, os.Remove(dirpath)) + } +} + +func setupGoldenFile(t *testing.T, content string) (string, func()) { + _ = os.Mkdir("testdata", 0755) + f, err := ioutil.TempFile("testdata", t.Name()+"-") + assert.NilError(t, err, "fail to create test golden file") + defer f.Close() + + _, err = f.Write([]byte(content)) + assert.NilError(t, err) + + return filepath.Base(f.Name()), func() { + assert.NilError(t, os.Remove(f.Name())) + } +} + +func TestStringFailure(t *testing.T) { + filename, clean := setupGoldenFile(t, "this is\nthe text") + defer clean() + + result := String("this is\nnot the text", filename)() + assert.Assert(t, !result.Success()) + assert.Equal(t, result.(failure).FailureMessage(), ` +--- expected ++++ actual +@@ -1,2 +1,2 @@ + this is +-the text ++not the text +`+failurePostamble(filename)) +} + +type failure interface { + FailureMessage() string +} + +func TestBytesFailure(t *testing.T) { + filename, clean := setupGoldenFile(t, "5556") + defer clean() + + result := Bytes([]byte("5555"), filename)() + assert.Assert(t, !result.Success()) + assert.Equal(t, result.(failure).FailureMessage(), + `[53 53 53 53] (actual) != [53 53 53 54] (expected)`+failurePostamble(filename)) +} + +func TestFlagUpdate(t *testing.T) { + assert.Assert(t, !FlagUpdate()) + setUpdateFlag(t) + assert.Assert(t, FlagUpdate()) +} + +func TestUpdate_CreatesPathsAndFile(t *testing.T) { + setUpdateFlag(t) + + dir := fs.NewDir(t, t.Name()) + + t.Run("creates the file", func(t *testing.T) { + filename := dir.Join("filename") + err := update(filename, nil) + assert.NilError(t, err) + + _, err = os.Stat(filename) + assert.NilError(t, err) + }) + + t.Run("creates directories", func(t *testing.T) { + filename := dir.Join("one/two/filename") + err := update(filename, nil) + assert.NilError(t, err) + + _, err = os.Stat(filename) + assert.NilError(t, err) + + t.Run("no error when directory exists", func(t *testing.T) { + err = update(filename, nil) + assert.NilError(t, err) + }) + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/command.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/command.go new file mode 100644 index 0000000..9613322 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/command.go @@ -0,0 +1,290 @@ +/*Package icmd executes binaries and provides convenient assertions for testing the results. + */ +package icmd // import "gotest.tools/v3/icmd" + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + exec "golang.org/x/sys/execabs" + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +type helperT interface { + Helper() +} + +// None is a token to inform Result.Assert that the output should be empty +const None = "[NOTHING]" + +type lockedBuffer struct { + m sync.RWMutex + buf bytes.Buffer +} + +func (buf *lockedBuffer) Write(b []byte) (int, error) { + buf.m.Lock() + defer buf.m.Unlock() + return buf.buf.Write(b) +} + +func (buf *lockedBuffer) String() string { + buf.m.RLock() + defer buf.m.RUnlock() + return buf.buf.String() +} + +// Result stores the result of running a command +type Result struct { + Cmd *exec.Cmd + ExitCode int + Error error + // Timeout is true if the command was killed because it ran for too long + Timeout bool + outBuffer *lockedBuffer + errBuffer *lockedBuffer +} + +// Assert compares the Result against the Expected struct, and fails the test if +// any of the expectations are not met. +// +// This function is equivalent to assert.Assert(t, result.Equal(exp)). +func (r *Result) Assert(t assert.TestingT, exp Expected) *Result { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.Assert(t, r.Equal(exp)) + return r +} + +// Equal compares the result to Expected. If the result doesn't match expected +// returns a formatted failure message with the command, stdout, stderr, exit code, +// and any failed expectations. +func (r *Result) Equal(exp Expected) cmp.Comparison { + return func() cmp.Result { + return cmp.ResultFromError(r.match(exp)) + } +} + +// Compare the result to Expected and return an error if they do not match. +func (r *Result) Compare(exp Expected) error { + return r.match(exp) +} + +func (r *Result) match(exp Expected) error { + errors := []string{} + add := func(format string, args ...interface{}) { + errors = append(errors, fmt.Sprintf(format, args...)) + } + + if exp.ExitCode != r.ExitCode { + add("ExitCode was %d expected %d", r.ExitCode, exp.ExitCode) + } + if exp.Timeout != r.Timeout { + if exp.Timeout { + add("Expected command to timeout") + } else { + add("Expected command to finish, but it hit the timeout") + } + } + if !matchOutput(exp.Out, r.Stdout()) { + add("Expected stdout to contain %q", exp.Out) + } + if !matchOutput(exp.Err, r.Stderr()) { + add("Expected stderr to contain %q", exp.Err) + } + switch { + // If a non-zero exit code is expected there is going to be an error. + // Don't require an error message as well as an exit code because the + // error message is going to be "exit status which is not useful + case exp.Error == "" && exp.ExitCode != 0: + case exp.Error == "" && r.Error != nil: + add("Expected no error") + case exp.Error != "" && r.Error == nil: + add("Expected error to contain %q, but there was no error", exp.Error) + case exp.Error != "" && !strings.Contains(r.Error.Error(), exp.Error): + add("Expected error to contain %q", exp.Error) + } + + if len(errors) == 0 { + return nil + } + return fmt.Errorf("%s\nFailures:\n%s", r, strings.Join(errors, "\n")) +} + +func matchOutput(expected string, actual string) bool { + switch expected { + case None: + return actual == "" + default: + return strings.Contains(actual, expected) + } +} + +func (r *Result) String() string { + var timeout string + if r.Timeout { + timeout = " (timeout)" + } + var errString string + if r.Error != nil { + errString = "\nError: " + r.Error.Error() + } + + return fmt.Sprintf(` +Command: %s +ExitCode: %d%s%s +Stdout: %v +Stderr: %v +`, + strings.Join(r.Cmd.Args, " "), + r.ExitCode, + timeout, + errString, + r.Stdout(), + r.Stderr()) +} + +// Expected is the expected output from a Command. This struct is compared to a +// Result struct by Result.Assert(). +type Expected struct { + ExitCode int + Timeout bool + Error string + Out string + Err string +} + +// Success is the default expected result. A Success result is one with a 0 +// ExitCode. +var Success = Expected{} + +// Stdout returns the stdout of the process as a string +func (r *Result) Stdout() string { + return r.outBuffer.String() +} + +// Stderr returns the stderr of the process as a string +func (r *Result) Stderr() string { + return r.errBuffer.String() +} + +// Combined returns the stdout and stderr combined into a single string +func (r *Result) Combined() string { + return r.outBuffer.String() + r.errBuffer.String() +} + +func (r *Result) setExitError(err error) { + if err == nil { + return + } + r.Error = err + r.ExitCode = processExitCode(err) +} + +// Cmd contains the arguments and options for a process to run as part of a test +// suite. +type Cmd struct { + Command []string + Timeout time.Duration + Stdin io.Reader + Stdout io.Writer + Dir string + Env []string + ExtraFiles []*os.File +} + +// Command create a simple Cmd with the specified command and arguments +func Command(command string, args ...string) Cmd { + return Cmd{Command: append([]string{command}, args...)} +} + +// RunCmd runs a command and returns a Result +func RunCmd(cmd Cmd, cmdOperators ...CmdOp) *Result { + for _, op := range cmdOperators { + op(&cmd) + } + result := StartCmd(cmd) + if result.Error != nil { + return result + } + return WaitOnCmd(cmd.Timeout, result) +} + +// RunCommand runs a command with default options, and returns a result +func RunCommand(command string, args ...string) *Result { + return RunCmd(Command(command, args...)) +} + +// StartCmd starts a command, but doesn't wait for it to finish +func StartCmd(cmd Cmd) *Result { + result := buildCmd(cmd) + if result.Error != nil { + return result + } + result.setExitError(result.Cmd.Start()) + return result +} + +// TODO: support exec.CommandContext +func buildCmd(cmd Cmd) *Result { + var execCmd *exec.Cmd + switch len(cmd.Command) { + case 1: + execCmd = exec.Command(cmd.Command[0]) + default: + execCmd = exec.Command(cmd.Command[0], cmd.Command[1:]...) + } + outBuffer := new(lockedBuffer) + errBuffer := new(lockedBuffer) + + execCmd.Stdin = cmd.Stdin + execCmd.Dir = cmd.Dir + execCmd.Env = cmd.Env + if cmd.Stdout != nil { + execCmd.Stdout = io.MultiWriter(outBuffer, cmd.Stdout) + } else { + execCmd.Stdout = outBuffer + } + execCmd.Stderr = errBuffer + execCmd.ExtraFiles = cmd.ExtraFiles + + return &Result{ + Cmd: execCmd, + outBuffer: outBuffer, + errBuffer: errBuffer, + } +} + +// WaitOnCmd waits for a command to complete. If timeout is non-nil then +// only wait until the timeout. +func WaitOnCmd(timeout time.Duration, result *Result) *Result { + if timeout == time.Duration(0) { + result.setExitError(result.Cmd.Wait()) + return result + } + + done := make(chan error, 1) + // Wait for command to exit in a goroutine + go func() { + done <- result.Cmd.Wait() + }() + + select { + case <-time.After(timeout): + killErr := result.Cmd.Process.Kill() + if killErr != nil { + fmt.Printf("failed to kill (pid=%d): %v\n", result.Cmd.Process.Pid, killErr) + } + result.Timeout = true + case err := <-done: + result.setExitError(err) + } + return result +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/command_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/command_test.go new file mode 100644 index 0000000..1a5fef9 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/command_test.go @@ -0,0 +1,181 @@ +package icmd + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + exec "golang.org/x/sys/execabs" + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" + "gotest.tools/v3/internal/maint" +) + +var ( + bindir = fs.NewDir(maint.T, "icmd-dir") + binname = bindir.Join("bin-stub") + pathext() + stubpath = filepath.FromSlash("./internal/stub") +) + +func pathext() string { + if runtime.GOOS == "windows" { + return ".exe" + } + return "" +} + +func TestMain(m *testing.M) { + exitcode := m.Run() + bindir.Remove() + os.Exit(exitcode) +} + +func buildStub(t assert.TestingT) { + if _, err := os.Stat(binname); err == nil { + return + } + result := RunCommand("go", "build", "-o", binname, stubpath) + result.Assert(t, Success) +} + +func TestRunCommandSuccess(t *testing.T) { + buildStub(t) + + result := RunCommand(binname) + result.Assert(t, Success) +} + +func TestRunCommandWithCombined(t *testing.T) { + buildStub(t) + + result := RunCommand(binname, "-warn") + result.Assert(t, Expected{}) + + assert.Equal(t, result.Combined(), "this is stdout\nthis is stderr\n") + assert.Equal(t, result.Stdout(), "this is stdout\n") + assert.Equal(t, result.Stderr(), "this is stderr\n") +} + +func TestRunCommandWithTimeoutFinished(t *testing.T) { + buildStub(t) + + result := RunCmd(Cmd{ + Command: []string{binname, "-sleep=1ms"}, + Timeout: 2 * time.Second, + }) + result.Assert(t, Expected{Out: "this is stdout"}) +} + +func TestRunCommandWithTimeoutKilled(t *testing.T) { + buildStub(t) + + command := []string{binname, "-sleep=200ms"} + result := RunCmd(Cmd{Command: command, Timeout: 30 * time.Millisecond}) + result.Assert(t, Expected{Timeout: true, Out: None, Err: None}) +} + +func TestRunCommandWithErrors(t *testing.T) { + buildStub(t) + + result := RunCommand("doesnotexists") + expected := `exec: "doesnotexists": executable file not found` + result.Assert(t, Expected{Out: None, Err: None, ExitCode: 127, Error: expected}) +} + +func TestRunCommandWithStdoutNoStderr(t *testing.T) { + buildStub(t) + + result := RunCommand(binname) + result.Assert(t, Expected{Out: "this is stdout\n", Err: None}) +} + +func TestRunCommandWithExitCode(t *testing.T) { + buildStub(t) + + result := RunCommand(binname, "-fail=99") + result.Assert(t, Expected{ + ExitCode: 99, + Error: "exit status 99", + }) +} + +func TestResult_Match_NotMatched(t *testing.T) { + result := &Result{ + Cmd: exec.Command("binary", "arg1"), + ExitCode: 99, + Error: errors.New("exit code 99"), + outBuffer: newLockedBuffer("the output"), + errBuffer: newLockedBuffer("the stderr"), + Timeout: true, + } + exp := Expected{ + ExitCode: 101, + Out: "Something else", + Err: None, + } + err := result.match(exp) + assert.ErrorContains(t, err, "Failures") + assert.Equal(t, err.Error(), expectedMatch) +} + +var expectedMatch = ` +Command: binary arg1 +ExitCode: 99 (timeout) +Error: exit code 99 +Stdout: the output +Stderr: the stderr + +Failures: +ExitCode was 99 expected 101 +Expected command to finish, but it hit the timeout +Expected stdout to contain "Something else" +Expected stderr to contain "[NOTHING]"` + +func newLockedBuffer(s string) *lockedBuffer { + return &lockedBuffer{buf: *bytes.NewBufferString(s)} +} + +func TestResult_Match_NotMatchedNoError(t *testing.T) { + result := &Result{ + Cmd: exec.Command("binary", "arg1"), + outBuffer: newLockedBuffer("the output"), + errBuffer: newLockedBuffer("the stderr"), + } + exp := Expected{ + ExitCode: 101, + Out: "Something else", + Err: None, + } + err := result.match(exp) + assert.ErrorContains(t, err, "Failures") + assert.Equal(t, err.Error(), expectedResultMatchNoMatch) +} + +var expectedResultMatchNoMatch = ` +Command: binary arg1 +ExitCode: 0 +Stdout: the output +Stderr: the stderr + +Failures: +ExitCode was 0 expected 101 +Expected stdout to contain "Something else" +Expected stderr to contain "[NOTHING]"` + +func TestResult_Match_Match(t *testing.T) { + result := &Result{ + Cmd: exec.Command("binary", "arg1"), + outBuffer: newLockedBuffer("the output"), + errBuffer: newLockedBuffer("the stderr"), + } + exp := Expected{ + Out: "the output", + Err: "the stderr", + } + err := result.match(exp) + assert.NilError(t, err) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/example_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/example_test.go new file mode 100644 index 0000000..216b82d --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/example_test.go @@ -0,0 +1,22 @@ +package icmd_test + +import ( + "testing" + + "gotest.tools/v3/icmd" +) + +var t = &testing.T{} + +func ExampleRunCommand() { + result := icmd.RunCommand("bash", "-c", "echo all good") + result.Assert(t, icmd.Success) +} + +func ExampleRunCmd() { + result := icmd.RunCmd(icmd.Command("cat", "/does/not/exist")) + result.Assert(t, icmd.Expected{ + ExitCode: 1, + Err: "cat: /does/not/exist: No such file or directory", + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/exitcode.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/exitcode.go new file mode 100644 index 0000000..2e98f86 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/exitcode.go @@ -0,0 +1,24 @@ +package icmd + +import ( + "errors" + + exec "golang.org/x/sys/execabs" +) + +func processExitCode(err error) int { + if err == nil { + return 0 + } + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ProcessState == nil { + return 0 + } + if code := exitErr.ProcessState.ExitCode(); code != -1 { + return code + } + } + return 127 +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/internal/stub/main.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/internal/stub/main.go new file mode 100644 index 0000000..a7fa1fd --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/internal/stub/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "flag" + "fmt" + "os" + "time" +) + +func main() { + sleep := flag.Duration("sleep", 0, "Sleep") + warn := flag.Bool("warn", false, "Warn") + fail := flag.Int("fail", 0, "Fail with code") + flag.Parse() + + if *sleep != 0 { + time.Sleep(*sleep) + } + + fmt.Println("this is stdout") + if *warn { + fmt.Fprintln(os.Stderr, "this is stderr") + } + + os.Exit(*fail) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/ops.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/ops.go new file mode 100644 index 0000000..35c3958 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/icmd/ops.go @@ -0,0 +1,46 @@ +package icmd + +import ( + "io" + "os" + "time" +) + +// CmdOp is an operation which modified a Cmd structure used to execute commands +type CmdOp func(*Cmd) + +// WithTimeout sets the timeout duration of the command +func WithTimeout(timeout time.Duration) CmdOp { + return func(c *Cmd) { + c.Timeout = timeout + } +} + +// WithEnv sets the environment variable of the command. +// Each arguments are in the form of KEY=VALUE +func WithEnv(env ...string) CmdOp { + return func(c *Cmd) { + c.Env = env + } +} + +// Dir sets the working directory of the command +func Dir(path string) CmdOp { + return func(c *Cmd) { + c.Dir = path + } +} + +// WithStdin sets the standard input of the command to the specified reader +func WithStdin(r io.Reader) CmdOp { + return func(c *Cmd) { + c.Stdin = r + } +} + +// WithExtraFile adds a file descriptor to the command +func WithExtraFile(f *os.File) CmdOp { + return func(c *Cmd) { + c.ExtraFiles = append(c.ExtraFiles, f) + } +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/assert/assert.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/assert/assert.go new file mode 100644 index 0000000..0d67751 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/assert/assert.go @@ -0,0 +1,160 @@ +package assert + +import ( + "fmt" + "go/ast" + "go/token" + "reflect" + + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/format" + "gotest.tools/v3/internal/source" +) + +// LogT is the subset of testing.T used by the assert package. +type LogT interface { + Log(args ...interface{}) +} + +type helperT interface { + Helper() +} + +const failureMessage = "assertion failed: " + +// Eval the comparison and print a failure messages if the comparison has failed. +func Eval( + t LogT, + argSelector argSelector, + comparison interface{}, + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + var success bool + switch check := comparison.(type) { + case bool: + if check { + return true + } + logFailureFromBool(t, msgAndArgs...) + + // Undocumented legacy comparison without Result type + case func() (success bool, message string): + success = runCompareFunc(t, check, msgAndArgs...) + + case nil: + return true + + case error: + msg := failureMsgFromError(check) + t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...)) + + case cmp.Comparison: + success = RunComparison(t, argSelector, check, msgAndArgs...) + + case func() cmp.Result: + success = RunComparison(t, argSelector, check, msgAndArgs...) + + default: + t.Log(fmt.Sprintf("invalid Comparison: %v (%T)", check, check)) + } + return success +} + +func runCompareFunc( + t LogT, + f func() (success bool, message string), + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if success, message := f(); !success { + t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) + return false + } + return true +} + +func logFailureFromBool(t LogT, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + const stackIndex = 3 // Assert()/Check(), assert(), logFailureFromBool() + args, err := source.CallExprArgs(stackIndex) + if err != nil { + t.Log(err.Error()) + return + } + + const comparisonArgIndex = 1 // Assert(t, comparison) + if len(args) <= comparisonArgIndex { + t.Log(failureMessage + "but assert failed to find the expression to print") + return + } + + msg, err := boolFailureMessage(args[comparisonArgIndex]) + if err != nil { + t.Log(err.Error()) + msg = "expression is false" + } + + t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...)) +} + +func failureMsgFromError(err error) string { + // Handle errors with non-nil types + v := reflect.ValueOf(err) + if v.Kind() == reflect.Ptr && v.IsNil() { + return fmt.Sprintf("error is not nil: error has type %T", err) + } + return "error is not nil: " + err.Error() +} + +func boolFailureMessage(expr ast.Expr) (string, error) { + if binaryExpr, ok := expr.(*ast.BinaryExpr); ok { + x, err := source.FormatNode(binaryExpr.X) + if err != nil { + return "", err + } + y, err := source.FormatNode(binaryExpr.Y) + if err != nil { + return "", err + } + + switch binaryExpr.Op { + case token.NEQ: + return x + " is " + y, nil + case token.EQL: + return x + " is not " + y, nil + case token.GTR: + return x + " is <= " + y, nil + case token.LSS: + return x + " is >= " + y, nil + case token.GEQ: + return x + " is less than " + y, nil + case token.LEQ: + return x + " is greater than " + y, nil + } + } + + if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.NOT { + x, err := source.FormatNode(unaryExpr.X) + if err != nil { + return "", err + } + return x + " is true", nil + } + + if ident, ok := expr.(*ast.Ident); ok { + return ident.Name + " is false", nil + } + + formatted, err := source.FormatNode(expr) + if err != nil { + return "", err + } + return "expression is false: " + formatted, nil +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/assert/result.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/assert/result.go new file mode 100644 index 0000000..3603206 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/assert/result.go @@ -0,0 +1,146 @@ +package assert + +import ( + "errors" + "fmt" + "go/ast" + + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/format" + "gotest.tools/v3/internal/source" +) + +// RunComparison and return Comparison.Success. If the comparison fails a messages +// will be printed using t.Log. +func RunComparison( + t LogT, + argSelector argSelector, + f cmp.Comparison, + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + result := f() + if result.Success() { + return true + } + + if source.Update { + if updater, ok := result.(updateExpected); ok { + const stackIndex = 3 // Assert/Check, assert, RunComparison + err := updater.UpdatedExpected(stackIndex) + switch { + case err == nil: + return true + case errors.Is(err, source.ErrNotFound): + // do nothing, fallthrough to regular failure message + default: + t.Log("failed to update source", err) + return false + } + } + } + + var message string + switch typed := result.(type) { + case resultWithComparisonArgs: + const stackIndex = 3 // Assert/Check, assert, RunComparison + args, err := source.CallExprArgs(stackIndex) + if err != nil { + t.Log(err.Error()) + } + message = typed.FailureMessage(filterPrintableExpr(argSelector(args))) + case resultBasic: + message = typed.FailureMessage() + default: + message = fmt.Sprintf("comparison returned invalid Result type: %T", result) + } + + t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) + return false +} + +type resultWithComparisonArgs interface { + FailureMessage(args []ast.Expr) string +} + +type resultBasic interface { + FailureMessage() string +} + +type updateExpected interface { + UpdatedExpected(stackIndex int) error +} + +// filterPrintableExpr filters the ast.Expr slice to only include Expr that are +// easy to read when printed and contain relevant information to an assertion. +// +// Ident and SelectorExpr are included because they print nicely and the variable +// names may provide additional context to their values. +// BasicLit and CompositeLit are excluded because their source is equivalent to +// their value, which is already available. +// Other types are ignored for now, but could be added if they are relevant. +func filterPrintableExpr(args []ast.Expr) []ast.Expr { + result := make([]ast.Expr, len(args)) + for i, arg := range args { + if isShortPrintableExpr(arg) { + result[i] = arg + continue + } + + if starExpr, ok := arg.(*ast.StarExpr); ok { + result[i] = starExpr.X + continue + } + } + return result +} + +func isShortPrintableExpr(expr ast.Expr) bool { + switch expr.(type) { + case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr: + return true + case *ast.BinaryExpr, *ast.UnaryExpr: + return true + default: + // CallExpr, ParenExpr, TypeAssertExpr, KeyValueExpr, StarExpr + return false + } +} + +type argSelector func([]ast.Expr) []ast.Expr + +// ArgsAfterT selects args starting at position 1. Used when the caller has a +// testing.T as the first argument, and the args to select should follow it. +func ArgsAfterT(args []ast.Expr) []ast.Expr { + if len(args) < 1 { + return nil + } + return args[1:] +} + +// ArgsFromComparisonCall selects args from the CallExpression at position 1. +// Used when the caller has a testing.T as the first argument, and the args to +// select are passed to the cmp.Comparison at position 1. +func ArgsFromComparisonCall(args []ast.Expr) []ast.Expr { + if len(args) <= 1 { + return nil + } + if callExpr, ok := args[1].(*ast.CallExpr); ok { + return callExpr.Args + } + return nil +} + +// ArgsAtZeroIndex selects args from the CallExpression at position 1. +// Used when the caller accepts a single cmp.Comparison argument. +func ArgsAtZeroIndex(args []ast.Expr) []ast.Expr { + if len(args) == 0 { + return nil + } + if callExpr, ok := args[0].(*ast.CallExpr); ok { + return callExpr.Args + } + return nil +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/cleanup/cleanup.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/cleanup/cleanup.go new file mode 100644 index 0000000..58206e5 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/cleanup/cleanup.go @@ -0,0 +1,48 @@ +/*Package cleanup handles migration to and support for the Go 1.14+ +testing.TB.Cleanup() function. +*/ +package cleanup + +import ( + "os" + "strings" +) + +type cleanupT interface { + Cleanup(f func()) +} + +// implemented by gotest.tools/x/subtest.TestContext +type addCleanupT interface { + AddCleanup(f func()) +} + +type logT interface { + Log(...interface{}) +} + +type helperT interface { + Helper() +} + +var noCleanup = strings.ToLower(os.Getenv("TEST_NOCLEANUP")) == "true" + +// Cleanup registers f as a cleanup function on t if any mechanisms are available. +// +// Skips registering f if TEST_NOCLEANUP is set to true. +func Cleanup(t logT, f func()) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if noCleanup { + t.Log("skipping cleanup because TEST_NOCLEANUP was enabled.") + return + } + if ct, ok := t.(cleanupT); ok { + ct.Cleanup(f) + return + } + if tc, ok := t.(addCleanupT); ok { + tc.AddCleanup(f) + } +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/difflib/LICENSE b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/difflib/LICENSE new file mode 100644 index 0000000..c67dad6 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/difflib/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, Patrick Mezard +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + The names of its contributors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/difflib/difflib.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/difflib/difflib.go new file mode 100644 index 0000000..9bf506b --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/difflib/difflib.go @@ -0,0 +1,423 @@ +/*Package difflib is a partial port of Python difflib module. + +Original source: https://github.com/pmezard/go-difflib + +This file is trimmed to only the parts used by this repository. +*/ +package difflib // import "gotest.tools/v3/internal/difflib" + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +// Match stores line numbers of size of match +type Match struct { + A int + B int + Size int +} + +// OpCode identifies the type of diff +type OpCode struct { + Tag byte + I1 int + I2 int + J1 int + J2 int +} + +// SequenceMatcher compares sequence of strings. The basic +// algorithm predates, and is a little fancier than, an algorithm +// published in the late 1980's by Ratcliff and Obershelp under the +// hyperbolic name "gestalt pattern matching". The basic idea is to find +// the longest contiguous matching subsequence that contains no "junk" +// elements (R-O doesn't address junk). The same idea is then applied +// recursively to the pieces of the sequences to the left and to the right +// of the matching subsequence. This does not yield minimal edit +// sequences, but does tend to yield matches that "look right" to people. +// +// SequenceMatcher tries to compute a "human-friendly diff" between two +// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the +// longest *contiguous* & junk-free matching subsequence. That's what +// catches peoples' eyes. The Windows(tm) windiff has another interesting +// notion, pairing up elements that appear uniquely in each sequence. +// That, and the method here, appear to yield more intuitive difference +// reports than does diff. This method appears to be the least vulnerable +// to synching up on blocks of "junk lines", though (like blank lines in +// ordinary text files, or maybe "

" lines in HTML files). That may be +// because this is the only method of the 3 that has a *concept* of +// "junk" . +// +// Timing: Basic R-O is cubic time worst case and quadratic time expected +// case. SequenceMatcher is quadratic time for the worst case and has +// expected-case behavior dependent in a complicated way on how many +// elements the sequences have in common; best case time is linear. +type SequenceMatcher struct { + a []string + b []string + b2j map[string][]int + IsJunk func(string) bool + autoJunk bool + bJunk map[string]struct{} + matchingBlocks []Match + fullBCount map[string]int + bPopular map[string]struct{} + opCodes []OpCode +} + +// NewMatcher returns a new SequenceMatcher +func NewMatcher(a, b []string) *SequenceMatcher { + m := SequenceMatcher{autoJunk: true} + m.SetSeqs(a, b) + return &m +} + +// SetSeqs sets two sequences to be compared. +func (m *SequenceMatcher) SetSeqs(a, b []string) { + m.SetSeq1(a) + m.SetSeq2(b) +} + +// SetSeq1 sets the first sequence to be compared. The second sequence to be compared is +// not changed. +// +// SequenceMatcher computes and caches detailed information about the second +// sequence, so if you want to compare one sequence S against many sequences, +// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other +// sequences. +// +// See also SetSeqs() and SetSeq2(). +func (m *SequenceMatcher) SetSeq1(a []string) { + if &a == &m.a { + return + } + m.a = a + m.matchingBlocks = nil + m.opCodes = nil +} + +// SetSeq2 sets the second sequence to be compared. The first sequence to be compared is +// not changed. +func (m *SequenceMatcher) SetSeq2(b []string) { + if &b == &m.b { + return + } + m.b = b + m.matchingBlocks = nil + m.opCodes = nil + m.fullBCount = nil + m.chainB() +} + +func (m *SequenceMatcher) chainB() { + // Populate line -> index mapping + b2j := map[string][]int{} + for i, s := range m.b { + indices := b2j[s] + indices = append(indices, i) + b2j[s] = indices + } + + // Purge junk elements + m.bJunk = map[string]struct{}{} + if m.IsJunk != nil { + junk := m.bJunk + for s := range b2j { + if m.IsJunk(s) { + junk[s] = struct{}{} + } + } + for s := range junk { + delete(b2j, s) + } + } + + // Purge remaining popular elements + popular := map[string]struct{}{} + n := len(m.b) + if m.autoJunk && n >= 200 { + ntest := n/100 + 1 + for s, indices := range b2j { + if len(indices) > ntest { + popular[s] = struct{}{} + } + } + for s := range popular { + delete(b2j, s) + } + } + m.bPopular = popular + m.b2j = b2j +} + +func (m *SequenceMatcher) isBJunk(s string) bool { + _, ok := m.bJunk[s] + return ok +} + +// Find longest matching block in a[alo:ahi] and b[blo:bhi]. +// +// If IsJunk is not defined: +// +// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi +// and for all (i',j',k') meeting those conditions, +// k >= k' +// i <= i' +// and if i == i', j <= j' +// +// In other words, of all maximal matching blocks, return one that +// starts earliest in a, and of all those maximal matching blocks that +// start earliest in a, return the one that starts earliest in b. +// +// If IsJunk is defined, first the longest matching block is +// determined as above, but with the additional restriction that no +// junk element appears in the block. Then that block is extended as +// far as possible by matching (only) junk elements on both sides. So +// the resulting block never matches on junk except as identical junk +// happens to be adjacent to an "interesting" match. +// +// If no blocks match, return (alo, blo, 0). +func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { + // CAUTION: stripping common prefix or suffix would be incorrect. + // E.g., + // ab + // acab + // Longest matching block is "ab", but if common prefix is + // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + // strip, so ends up claiming that ab is changed to acab by + // inserting "ca" in the middle. That's minimal but unintuitive: + // "it's obvious" that someone inserted "ac" at the front. + // Windiff ends up at the same place as diff, but by pairing up + // the unique 'b's and then matching the first two 'a's. + besti, bestj, bestsize := alo, blo, 0 + + // find longest junk-free match + // during an iteration of the loop, j2len[j] = length of longest + // junk-free match ending with a[i-1] and b[j] + j2len := map[int]int{} + for i := alo; i != ahi; i++ { + // look at all instances of a[i] in b; note that because + // b2j has no junk keys, the loop is skipped if a[i] is junk + newj2len := map[int]int{} + for _, j := range m.b2j[m.a[i]] { + // a[i] matches b[j] + if j < blo { + continue + } + if j >= bhi { + break + } + k := j2len[j-1] + 1 + newj2len[j] = k + if k > bestsize { + besti, bestj, bestsize = i-k+1, j-k+1, k + } + } + j2len = newj2len + } + + // Extend the best by non-junk elements on each end. In particular, + // "popular" non-junk elements aren't in b2j, which greatly speeds + // the inner loop above, but also means "the best" match so far + // doesn't contain any junk *or* popular non-junk elements. + for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + !m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + // Now that we have a wholly interesting match (albeit possibly + // empty!), we may as well suck up the matching junk on each + // side of it too. Can't think of a good reason not to, and it + // saves post-processing the (possibly considerable) expense of + // figuring out what to do with it. In the case of an empty + // interesting match, this is clearly the right thing to do, + // because no other kind of match is possible in the regions. + for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + return Match{A: besti, B: bestj, Size: bestsize} +} + +// GetMatchingBlocks returns a list of triples describing matching subsequences. +// +// Each triple is of the form (i, j, n), and means that +// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in +// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are +// adjacent triples in the list, and the second is not the last triple in the +// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe +// adjacent equal blocks. +// +// The last triple is a dummy, (len(a), len(b), 0), and is the only +// triple with n==0. +func (m *SequenceMatcher) GetMatchingBlocks() []Match { + if m.matchingBlocks != nil { + return m.matchingBlocks + } + + var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match + matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match { + match := m.findLongestMatch(alo, ahi, blo, bhi) + i, j, k := match.A, match.B, match.Size + if match.Size > 0 { + if alo < i && blo < j { + matched = matchBlocks(alo, i, blo, j, matched) + } + matched = append(matched, match) + if i+k < ahi && j+k < bhi { + matched = matchBlocks(i+k, ahi, j+k, bhi, matched) + } + } + return matched + } + matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) + + // It's possible that we have adjacent equal blocks in the + // matching_blocks list now. + nonAdjacent := []Match{} + i1, j1, k1 := 0, 0, 0 + for _, b := range matched { + // Is this block adjacent to i1, j1, k1? + i2, j2, k2 := b.A, b.B, b.Size + if i1+k1 == i2 && j1+k1 == j2 { + // Yes, so collapse them -- this just increases the length of + // the first block by the length of the second, and the first + // block so lengthened remains the block to compare against. + k1 += k2 + } else { + // Not adjacent. Remember the first block (k1==0 means it's + // the dummy we started with), and make the second block the + // new block to compare against. + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + i1, j1, k1 = i2, j2, k2 + } + } + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + + nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0}) + m.matchingBlocks = nonAdjacent + return m.matchingBlocks +} + +// GetOpCodes returns a list of 5-tuples describing how to turn a into b. +// +// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple +// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the +// tuple preceding it, and likewise for j1 == the previous j2. +// +// The tags are characters, with these meanings: +// +// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] +// +// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. +// +// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. +// +// 'e' (equal): a[i1:i2] == b[j1:j2] +func (m *SequenceMatcher) GetOpCodes() []OpCode { + if m.opCodes != nil { + return m.opCodes + } + i, j := 0, 0 + matching := m.GetMatchingBlocks() + opCodes := make([]OpCode, 0, len(matching)) + for _, m := range matching { + // invariant: we've pumped out correct diffs to change + // a[:i] into b[:j], and the next matching block is + // a[ai:ai+size] == b[bj:bj+size]. So we need to pump + // out a diff to change a[i:ai] into b[j:bj], pump out + // the matching block, and move (i,j) beyond the match + ai, bj, size := m.A, m.B, m.Size + tag := byte(0) + if i < ai && j < bj { + tag = 'r' + } else if i < ai { + tag = 'd' + } else if j < bj { + tag = 'i' + } + if tag > 0 { + opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) + } + i, j = ai+size, bj+size + // the list of matching blocks is terminated by a + // sentinel with size 0 + if size > 0 { + opCodes = append(opCodes, OpCode{'e', ai, i, bj, j}) + } + } + m.opCodes = opCodes + return m.opCodes +} + +// GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes. +// +// Return a generator of groups with up to n lines of context. +// Each group is in the same format as returned by GetOpCodes(). +func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { + if n < 0 { + n = 3 + } + codes := m.GetOpCodes() + if len(codes) == 0 { + codes = []OpCode{{'e', 0, 1, 0, 1}} + } + // Fixup leading and trailing groups if they show no changes. + if codes[0].Tag == 'e' { + c := codes[0] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} + } + if codes[len(codes)-1].Tag == 'e' { + c := codes[len(codes)-1] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} + } + nn := n + n + groups := [][]OpCode{} + group := []OpCode{} + for _, c := range codes { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + // End the current group and start a new one whenever + // there is a large range with no changes. + if c.Tag == 'e' && i2-i1 > nn { + group = append(group, OpCode{c.Tag, i1, min(i2, i1+n), + j1, min(j2, j1+n)}) + groups = append(groups, group) + group = []OpCode{} + i1, j1 = max(i1, i2-n), max(j1, j2-n) + } + group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) + } + if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { + groups = append(groups, group) + } + return groups +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/diff.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/diff.go new file mode 100644 index 0000000..9897d4b --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/diff.go @@ -0,0 +1,161 @@ +package format + +import ( + "bytes" + "fmt" + "strings" + "unicode" + + "gotest.tools/v3/internal/difflib" +) + +const ( + contextLines = 2 +) + +// DiffConfig for a unified diff +type DiffConfig struct { + A string + B string + From string + To string +} + +// UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better +// support for showing the whitespace differences. +func UnifiedDiff(conf DiffConfig) string { + a := strings.SplitAfter(conf.A, "\n") + b := strings.SplitAfter(conf.B, "\n") + groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines) + if len(groups) == 0 { + return "" + } + + buf := new(bytes.Buffer) + writeFormat := func(format string, args ...interface{}) { + buf.WriteString(fmt.Sprintf(format, args...)) + } + writeLine := func(prefix string, s string) { + buf.WriteString(prefix + s) + } + if hasWhitespaceDiffLines(groups, a, b) { + writeLine = visibleWhitespaceLine(writeLine) + } + formatHeader(writeFormat, conf) + for _, group := range groups { + formatRangeLine(writeFormat, group) + for _, opCode := range group { + in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2] + switch opCode.Tag { + case 'e': + formatLines(writeLine, " ", in) + case 'r': + formatLines(writeLine, "-", in) + formatLines(writeLine, "+", out) + case 'd': + formatLines(writeLine, "-", in) + case 'i': + formatLines(writeLine, "+", out) + } + } + } + return buf.String() +} + +// hasWhitespaceDiffLines returns true if any diff groups is only different +// because of whitespace characters. +func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool { + for _, group := range groups { + in, out := new(bytes.Buffer), new(bytes.Buffer) + for _, opCode := range group { + if opCode.Tag == 'e' { + continue + } + for _, line := range a[opCode.I1:opCode.I2] { + in.WriteString(line) + } + for _, line := range b[opCode.J1:opCode.J2] { + out.WriteString(line) + } + } + if removeWhitespace(in.String()) == removeWhitespace(out.String()) { + return true + } + } + return false +} + +func removeWhitespace(s string) string { + var result []rune + for _, r := range s { + if !unicode.IsSpace(r) { + result = append(result, r) + } + } + return string(result) +} + +func visibleWhitespaceLine(ws func(string, string)) func(string, string) { + mapToVisibleSpace := func(r rune) rune { + switch r { + case '\n': + case ' ': + return '·' + case '\t': + return '▷' + case '\v': + return '▽' + case '\r': + return '↵' + case '\f': + return '↓' + default: + if unicode.IsSpace(r) { + return '�' + } + } + return r + } + return func(prefix, s string) { + ws(prefix, strings.Map(mapToVisibleSpace, s)) + } +} + +func formatHeader(wf func(string, ...interface{}), conf DiffConfig) { + if conf.From != "" || conf.To != "" { + wf("--- %s\n", conf.From) + wf("+++ %s\n", conf.To) + } +} + +func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) { + first, last := group[0], group[len(group)-1] + range1 := formatRangeUnified(first.I1, last.I2) + range2 := formatRangeUnified(first.J1, last.J2) + wf("@@ -%s +%s @@\n", range1, range2) +} + +// Convert range to the "ed" format +func formatRangeUnified(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 1 { + return fmt.Sprintf("%d", beginning) + } + if length == 0 { + beginning-- // empty ranges begin at line just before the range + } + return fmt.Sprintf("%d,%d", beginning, length) +} + +func formatLines(writeLine func(string, string), prefix string, lines []string) { + for _, line := range lines { + writeLine(prefix, line) + } + // Add a newline if the last line is missing one so that the diff displays + // properly. + if !strings.HasSuffix(lines[len(lines)-1], "\n") { + writeLine("", "\n") + } +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/diff_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/diff_test.go new file mode 100644 index 0000000..4986635 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/diff_test.go @@ -0,0 +1,71 @@ +package format_test + +import ( + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/golden" + "gotest.tools/v3/internal/format" +) + +func TestUnifiedDiff(t *testing.T) { + var testcases = []struct { + name string + a string + b string + expected string + from string + to string + }{ + { + name: "empty diff", + a: "a\nb\nc", + b: "a\nb\nc", + from: "from", + to: "to", + }, + { + name: "one diff with header", + a: "a\nxyz\nc", + b: "a\nb\nc", + from: "from", + to: "to", + expected: "one-diff-with-header.golden", + }, + { + name: "many diffs", + a: "a123\nxyz\nc\nbaba\nz\nt\nj2j2\nok\nok\ndone\n", + b: "a123\nxyz\nc\nabab\nz\nt\nj2j2\nok\nok\n", + expected: "many-diff.golden", + }, + { + name: "no trailing newline", + a: "a123\nxyz\nc\nbaba\nz\nt\nj2j2\nok\nok\ndone\n", + b: "a123\nxyz\nc\nabab\nz\nt\nj2j2\nok\nok", + expected: "many-diff-no-trailing-newline.golden", + }, + { + name: "whitespace diff", + a: " something\n something\n \v\r\n", + b: " something\n\tsomething\n \n", + expected: "whitespace-diff.golden", + }, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + diff := format.UnifiedDiff(format.DiffConfig{ + A: testcase.a, + B: testcase.b, + From: testcase.from, + To: testcase.to, + }) + + if testcase.expected != "" { + assert.Assert(t, golden.String(diff, testcase.expected)) + return + } + assert.Equal(t, diff, "") + }) + } +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/format.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/format.go new file mode 100644 index 0000000..5097e4b --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/format.go @@ -0,0 +1,27 @@ +package format // import "gotest.tools/v3/internal/format" + +import "fmt" + +// Message accepts a msgAndArgs varargs and formats it using fmt.Sprintf +func Message(msgAndArgs ...interface{}) string { + switch len(msgAndArgs) { + case 0: + return "" + case 1: + return fmt.Sprintf("%v", msgAndArgs[0]) + default: + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } +} + +// WithCustomMessage accepts one or two messages and formats them appropriately +func WithCustomMessage(source string, msgAndArgs ...interface{}) string { + custom := Message(msgAndArgs...) + switch { + case custom == "": + return source + case source == "": + return custom + } + return fmt.Sprintf("%s: %s", source, custom) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/format_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/format_test.go new file mode 100644 index 0000000..6404ad3 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/format_test.go @@ -0,0 +1,62 @@ +package format_test + +import ( + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/internal/format" +) + +func TestMessage(t *testing.T) { + var testcases = []struct { + doc string + args []interface{} + expected string + }{ + { + doc: "none", + }, + { + doc: "single string", + args: args("foo"), + expected: "foo", + }, + { + doc: "single non-string", + args: args(123), + expected: "123", + }, + { + doc: "format string and args", + args: args("%s %v", "a", 3), + expected: "a 3", + }, + } + + for _, tc := range testcases { + t.Run(tc.doc, func(t *testing.T) { + assert.Equal(t, format.Message(tc.args...), tc.expected) + }) + } +} + +func args(a ...interface{}) []interface{} { + return a +} + +func TestWithCustomMessage(t *testing.T) { + t.Run("only custom", func(t *testing.T) { + msg := format.WithCustomMessage("", "extra") + assert.Equal(t, msg, "extra") + }) + + t.Run("only source", func(t *testing.T) { + msg := format.WithCustomMessage("source") + assert.Equal(t, msg, "source") + }) + + t.Run("source and custom", func(t *testing.T) { + msg := format.WithCustomMessage("source", "extra") + assert.Equal(t, msg, "source: extra") + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/many-diff-no-trailing-newline.golden b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/many-diff-no-trailing-newline.golden new file mode 100644 index 0000000..9518b0d --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/many-diff-no-trailing-newline.golden @@ -0,0 +1,13 @@ +@@ -2,10 +2,8 @@ + xyz + c +-baba ++abab + z + t + j2j2 + ok +-ok +-done +- ++ok diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/many-diff.golden b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/many-diff.golden new file mode 100644 index 0000000..8202cbe --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/many-diff.golden @@ -0,0 +1,12 @@ +@@ -2,5 +2,5 @@ + xyz + c +-baba ++abab + z + t +@@ -8,4 +8,3 @@ + ok + ok +-done + diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/one-diff-with-header.golden b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/one-diff-with-header.golden new file mode 100644 index 0000000..6012d3f --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/one-diff-with-header.golden @@ -0,0 +1,7 @@ +--- from ++++ to +@@ -1,3 +1,3 @@ + a +-xyz ++b + c diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/whitespace-diff.golden b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/whitespace-diff.golden new file mode 100644 index 0000000..de9a187 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/format/testdata/whitespace-diff.golden @@ -0,0 +1,7 @@ +@@ -1,4 +1,4 @@ + ··something +-······something +-····▽↵ ++▷something ++·· + diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/maint/maint.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/maint/maint.go new file mode 100644 index 0000000..6d6a7cf --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/maint/maint.go @@ -0,0 +1,28 @@ +package maint // import "gotest.tools/v3/internal/maint" + +import ( + "fmt" + "os" +) + +// T provides an implementation of assert.TestingT which uses os.Exit, and +// fmt.Println. This implementation can be used outside of test cases to provide +// assert.TestingT, for example in a TestMain. +var T = t{} + +type t struct{} + +// FailNow exits with a non-zero code +func (t t) FailNow() { + os.Exit(1) +} + +// Fail exits with a non-zero code +func (t t) Fail() { + os.Exit(2) +} + +// Log args by printing them to stdout +func (t t) Log(args ...interface{}) { + fmt.Println(args...) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/defers.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/defers.go new file mode 100644 index 0000000..392d9fe --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/defers.go @@ -0,0 +1,52 @@ +package source + +import ( + "fmt" + "go/ast" + "go/token" +) + +func scanToDeferLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node { + var matchedNode ast.Node + ast.Inspect(node, func(node ast.Node) bool { + switch { + case node == nil || matchedNode != nil: + return false + case fileset.Position(node.End()).Line == lineNum: + if funcLit, ok := node.(*ast.FuncLit); ok { + matchedNode = funcLit + return false + } + } + return true + }) + debug("defer line node: %s", debugFormatNode{matchedNode}) + return matchedNode +} + +func guessDefer(node ast.Node) (ast.Node, error) { + defers := collectDefers(node) + switch len(defers) { + case 0: + return nil, fmt.Errorf("failed to find expression in defer") + case 1: + return defers[0].Call, nil + default: + return nil, fmt.Errorf( + "ambiguous call expression: multiple (%d) defers in call block", + len(defers)) + } +} + +func collectDefers(node ast.Node) []*ast.DeferStmt { + var defers []*ast.DeferStmt + ast.Inspect(node, func(node ast.Node) bool { + if d, ok := node.(*ast.DeferStmt); ok { + defers = append(defers, d) + debug("defer: %s", debugFormatNode{d}) + return false + } + return true + }) + return defers +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/source.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/source.go new file mode 100644 index 0000000..a3f7008 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/source.go @@ -0,0 +1,147 @@ +package source // import "gotest.tools/v3/internal/source" + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "runtime" +) + +// FormattedCallExprArg returns the argument from an ast.CallExpr at the +// index in the call stack. The argument is formatted using FormatNode. +func FormattedCallExprArg(stackIndex int, argPos int) (string, error) { + args, err := CallExprArgs(stackIndex + 1) + if err != nil { + return "", err + } + if argPos >= len(args) { + return "", errors.New("failed to find expression") + } + return FormatNode(args[argPos]) +} + +// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at +// the index in the call stack. +func CallExprArgs(stackIndex int) ([]ast.Expr, error) { + _, filename, line, ok := runtime.Caller(stackIndex + 1) + if !ok { + return nil, errors.New("failed to get call stack") + } + debug("call stack position: %s:%d", filename, line) + + fileset := token.NewFileSet() + astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors) + if err != nil { + return nil, fmt.Errorf("failed to parse source file %s: %w", filename, err) + } + + expr, err := getCallExprArgs(fileset, astFile, line) + if err != nil { + return nil, fmt.Errorf("call from %s:%d: %w", filename, line, err) + } + return expr, nil +} + +func getNodeAtLine(fileset *token.FileSet, astFile ast.Node, lineNum int) (ast.Node, error) { + if node := scanToLine(fileset, astFile, lineNum); node != nil { + return node, nil + } + if node := scanToDeferLine(fileset, astFile, lineNum); node != nil { + node, err := guessDefer(node) + if err != nil || node != nil { + return node, err + } + } + return nil, nil +} + +func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node { + var matchedNode ast.Node + ast.Inspect(node, func(node ast.Node) bool { + switch { + case node == nil || matchedNode != nil: + return false + case fileset.Position(node.Pos()).Line == lineNum: + matchedNode = node + return false + } + return true + }) + return matchedNode +} + +func getCallExprArgs(fileset *token.FileSet, astFile ast.Node, line int) ([]ast.Expr, error) { + node, err := getNodeAtLine(fileset, astFile, line) + switch { + case err != nil: + return nil, err + case node == nil: + return nil, fmt.Errorf("failed to find an expression") + } + + debug("found node: %s", debugFormatNode{node}) + + visitor := &callExprVisitor{} + ast.Walk(visitor, node) + if visitor.expr == nil { + return nil, errors.New("failed to find call expression") + } + debug("callExpr: %s", debugFormatNode{visitor.expr}) + return visitor.expr.Args, nil +} + +type callExprVisitor struct { + expr *ast.CallExpr +} + +func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor { + if v.expr != nil || node == nil { + return nil + } + debug("visit: %s", debugFormatNode{node}) + + switch typed := node.(type) { + case *ast.CallExpr: + v.expr = typed + return nil + case *ast.DeferStmt: + ast.Walk(v, typed.Call.Fun) + return nil + } + return v +} + +// FormatNode using go/format.Node and return the result as a string +func FormatNode(node ast.Node) (string, error) { + buf := new(bytes.Buffer) + err := format.Node(buf, token.NewFileSet(), node) + return buf.String(), err +} + +var debugEnabled = os.Getenv("GOTESTTOOLS_DEBUG") != "" + +func debug(format string, args ...interface{}) { + if debugEnabled { + fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...) + } +} + +type debugFormatNode struct { + ast.Node +} + +func (n debugFormatNode) String() string { + if n.Node == nil { + return "none" + } + out, err := FormatNode(n.Node) + if err != nil { + return fmt.Sprintf("failed to format %s: %s", n.Node, err) + } + return fmt.Sprintf("(%T) %s", n.Node, out) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/source_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/source_test.go new file mode 100644 index 0000000..3c21819 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/source_test.go @@ -0,0 +1,94 @@ +package source_test + +// using a separate package for test to avoid circular imports with the assert +// package + +import ( + "fmt" + "runtime" + "strings" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/internal/source" + "gotest.tools/v3/skip" +) + +func TestFormattedCallExprArg_SingleLine(t *testing.T) { + msg, err := shim("not", "this", "this text") + assert.NilError(t, err) + assert.Equal(t, `"this text"`, msg) +} + +func TestFormattedCallExprArg_MultiLine(t *testing.T) { + msg, err := shim( + "first", + "second", + "this text", + ) + assert.NilError(t, err) + assert.Equal(t, `"this text"`, msg) +} + +func TestFormattedCallExprArg_IfStatement(t *testing.T) { + if msg, err := shim( + "first", + "second", + "this text", + ); true { + assert.NilError(t, err) + assert.Equal(t, `"this text"`, msg) + } +} + +func shim(_, _, _ string) (string, error) { + return source.FormattedCallExprArg(1, 2) +} + +func TestFormattedCallExprArg_InDefer(t *testing.T) { + skip.If(t, isGoVersion18) + cap := &capture{} + func() { + defer cap.shim("first", "second") + }() + + assert.NilError(t, cap.err) + assert.Equal(t, cap.value, `"second"`) +} + +func isGoVersion18() bool { + return strings.HasPrefix(runtime.Version(), "go1.8.") +} + +type capture struct { + value string + err error +} + +func (c *capture) shim(_, _ string) { + c.value, c.err = source.FormattedCallExprArg(1, 1) +} + +func TestFormattedCallExprArg_InAnonymousDefer(t *testing.T) { + cap := &capture{} + func() { + fmt.Println() + defer fmt.Println() + defer func() { cap.shim("first", "second") }() + }() + + assert.NilError(t, cap.err) + assert.Equal(t, cap.value, `"second"`) +} + +func TestFormattedCallExprArg_InDeferMultipleDefers(t *testing.T) { + skip.If(t, isGoVersion18) + cap := &capture{} + func() { + fmt.Println() + defer fmt.Println() + defer cap.shim("first", "second") + }() + + assert.ErrorContains(t, cap.err, "ambiguous call expression") +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/update.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/update.go new file mode 100644 index 0000000..bd9678b --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/update.go @@ -0,0 +1,138 @@ +package source + +import ( + "bytes" + "errors" + "flag" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "runtime" + "strings" +) + +// Update is set by the -update flag. It indicates the user running the tests +// would like to update any golden values. +var Update bool + +func init() { + flag.BoolVar(&Update, "update", false, "update golden values") +} + +// ErrNotFound indicates that UpdateExpectedValue failed to find the +// variable to update, likely because it is not a package level variable. +var ErrNotFound = fmt.Errorf("failed to find variable for update of golden value") + +// UpdateExpectedValue looks for a package-level variable with a name that +// starts with expected in the arguments to the caller. If the variable is +// found, the value of the variable will be updated to value of the other +// argument to the caller. +func UpdateExpectedValue(stackIndex int, x, y interface{}) error { + _, filename, line, ok := runtime.Caller(stackIndex + 1) + if !ok { + return errors.New("failed to get call stack") + } + debug("call stack position: %s:%d", filename, line) + + fileset := token.NewFileSet() + astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors|parser.ParseComments) + if err != nil { + return fmt.Errorf("failed to parse source file %s: %w", filename, err) + } + + expr, err := getCallExprArgs(fileset, astFile, line) + if err != nil { + return fmt.Errorf("call from %s:%d: %w", filename, line, err) + } + + if len(expr) < 3 { + debug("not enough arguments %d: %v", + len(expr), debugFormatNode{Node: &ast.CallExpr{Args: expr}}) + return ErrNotFound + } + + argIndex, varName := getVarNameForExpectedValueArg(expr) + if argIndex < 0 || varName == "" { + debug("no arguments started with the word 'expected': %v", + debugFormatNode{Node: &ast.CallExpr{Args: expr}}) + return ErrNotFound + } + + value := x + if argIndex == 1 { + value = y + } + + strValue, ok := value.(string) + if !ok { + debug("value must be type string, got %T", value) + return ErrNotFound + } + return UpdateVariable(filename, fileset, astFile, varName, strValue) +} + +// UpdateVariable writes to filename the contents of astFile with the value of +// the variable updated to value. +func UpdateVariable( + filename string, + fileset *token.FileSet, + astFile *ast.File, + varName string, + value string, +) error { + obj := astFile.Scope.Objects[varName] + if obj == nil { + return ErrNotFound + } + if obj.Kind != ast.Con && obj.Kind != ast.Var { + debug("can only update var and const, found %v", obj.Kind) + return ErrNotFound + } + + spec, ok := obj.Decl.(*ast.ValueSpec) + if !ok { + debug("can only update *ast.ValueSpec, found %T", obj.Decl) + return ErrNotFound + } + if len(spec.Names) != 1 { + debug("more than one name in ast.ValueSpec") + return ErrNotFound + } + + spec.Values[0] = &ast.BasicLit{ + Kind: token.STRING, + Value: "`" + value + "`", + } + + var buf bytes.Buffer + if err := format.Node(&buf, fileset, astFile); err != nil { + return fmt.Errorf("failed to format file after update: %w", err) + } + + fh, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to open file %v: %w", filename, err) + } + if _, err = fh.Write(buf.Bytes()); err != nil { + return fmt.Errorf("failed to write file %v: %w", filename, err) + } + if err := fh.Sync(); err != nil { + return fmt.Errorf("failed to sync file %v: %w", filename, err) + } + return nil +} + +func getVarNameForExpectedValueArg(expr []ast.Expr) (int, string) { + for i := 1; i < 3; i++ { + switch e := expr[i].(type) { + case *ast.Ident: + if strings.HasPrefix(strings.ToLower(e.Name), "expected") { + return i, e.Name + } + } + } + return -1, "" +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/version.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/version.go new file mode 100644 index 0000000..5fa8a90 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/internal/source/version.go @@ -0,0 +1,35 @@ +package source + +import ( + "runtime" + "strconv" + "strings" +) + +// GoVersionLessThan returns true if runtime.Version() is semantically less than +// version major.minor. Returns false if a release version can not be parsed from +// runtime.Version(). +func GoVersionLessThan(major, minor int64) bool { + version := runtime.Version() + // not a release version + if !strings.HasPrefix(version, "go") { + return false + } + version = strings.TrimPrefix(version, "go") + parts := strings.Split(version, ".") + if len(parts) < 2 { + return false + } + rMajor, err := strconv.ParseInt(parts[0], 10, 32) + if err != nil { + return false + } + if rMajor != major { + return rMajor < major + } + rMinor, err := strconv.ParseInt(parts[1], 10, 32) + if err != nil { + return false + } + return rMinor < minor +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/pkg.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/pkg.go new file mode 100644 index 0000000..e7a858a --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/pkg.go @@ -0,0 +1,4 @@ +/*Package gotesttools is a collection of packages to augment `testing` and +support common patterns. +*/ +package gotesttools // import "gotest.tools/v3" diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/check.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/check.go new file mode 100644 index 0000000..46880f5 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/check.go @@ -0,0 +1,47 @@ +package poll + +import ( + "net" + "os" +) + +// Check is a function which will be used as check for the WaitOn method. +type Check func(t LogT) Result + +// FileExists looks on filesystem and check that path exists. +func FileExists(path string) Check { + return func(t LogT) Result { + if h, ok := t.(helperT); ok { + h.Helper() + } + + _, err := os.Stat(path) + switch { + case os.IsNotExist(err): + t.Logf("waiting on file %s to exist", path) + return Continue("file %s does not exist", path) + case err != nil: + return Error(err) + default: + return Success() + } + } +} + +// Connection try to open a connection to the address on the +// named network. See net.Dial for a description of the network and +// address parameters. +func Connection(network, address string) Check { + return func(t LogT) Result { + if h, ok := t.(helperT); ok { + h.Helper() + } + + _, err := net.Dial(network, address) + if err != nil { + t.Logf("waiting on socket %s://%s to be available...", network, address) + return Continue("socket %s://%s not available", network, address) + } + return Success() + } +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/check_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/check_test.go new file mode 100644 index 0000000..c853838 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/check_test.go @@ -0,0 +1,42 @@ +package poll + +import ( + "fmt" + "os" + "testing" + + "gotest.tools/v3/assert" +) + +func TestWaitOnFile(t *testing.T) { + fakeFilePath := "./fakefile" + + check := FileExists(fakeFilePath) + + t.Run("file does not exist", func(t *testing.T) { + r := check(t) + assert.Assert(t, !r.Done()) + assert.Equal(t, r.Message(), fmt.Sprintf("file %s does not exist", fakeFilePath)) + }) + + os.Create(fakeFilePath) + defer os.Remove(fakeFilePath) + + t.Run("file exists", func(t *testing.T) { + assert.Assert(t, check(t).Done()) + }) +} + +func TestWaitOnSocketWithTimeout(t *testing.T) { + t.Run("connection to unavailable address", func(t *testing.T) { + check := Connection("tcp", "foo.bar:55555") + r := check(t) + assert.Assert(t, !r.Done()) + assert.Equal(t, r.Message(), "socket tcp://foo.bar:55555 not available") + }) + + t.Run("connection to ", func(t *testing.T) { + check := Connection("tcp", "google.com:80") + assert.Assert(t, check(t).Done()) + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/example_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/example_test.go new file mode 100644 index 0000000..2e3c32d --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/example_test.go @@ -0,0 +1,45 @@ +package poll_test + +import ( + "fmt" + "time" + + "gotest.tools/v3/poll" +) + +var t poll.TestingT + +func numOfProcesses() (int, error) { + return 0, nil +} + +func ExampleWaitOn() { + desired := 10 + + check := func(t poll.LogT) poll.Result { + actual, err := numOfProcesses() + if err != nil { + return poll.Error(fmt.Errorf("failed to get number of processes: %w", err)) + } + if actual == desired { + return poll.Success() + } + t.Logf("waiting on process count to be %d...", desired) + return poll.Continue("number of processes is %d, not %d", actual, desired) + } + + poll.WaitOn(t, check) +} + +func isDesiredState() bool { return false } +func getState() string { return "" } + +func ExampleSettingOp() { + check := func(t poll.LogT) poll.Result { + if isDesiredState() { + return poll.Success() + } + return poll.Continue("state is: %s", getState()) + } + poll.WaitOn(t, check, poll.WithTimeout(30*time.Second), poll.WithDelay(15*time.Millisecond)) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/poll.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/poll.go new file mode 100644 index 0000000..29c5b40 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/poll.go @@ -0,0 +1,171 @@ +/*Package poll provides tools for testing asynchronous code. + */ +package poll // import "gotest.tools/v3/poll" + +import ( + "fmt" + "strings" + "time" + + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/assert" +) + +// TestingT is the subset of testing.T used by WaitOn +type TestingT interface { + LogT + Fatalf(format string, args ...interface{}) +} + +// LogT is a logging interface that is passed to the WaitOn check function +type LogT interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) +} + +type helperT interface { + Helper() +} + +// Settings are used to configure the behaviour of WaitOn +type Settings struct { + // Timeout is the maximum time to wait for the condition. Defaults to 10s. + Timeout time.Duration + // Delay is the time to sleep between checking the condition. Defaults to + // 100ms. + Delay time.Duration +} + +func defaultConfig() *Settings { + return &Settings{Timeout: 10 * time.Second, Delay: 100 * time.Millisecond} +} + +// SettingOp is a function which accepts and modifies Settings +type SettingOp func(config *Settings) + +// WithDelay sets the delay to wait between polls +func WithDelay(delay time.Duration) SettingOp { + return func(config *Settings) { + config.Delay = delay + } +} + +// WithTimeout sets the timeout +func WithTimeout(timeout time.Duration) SettingOp { + return func(config *Settings) { + config.Timeout = timeout + } +} + +// Result of a check performed by WaitOn +type Result interface { + // Error indicates that the check failed and polling should stop, and the + // the has failed + Error() error + // Done indicates that polling should stop, and the test should proceed + Done() bool + // Message provides the most recent state when polling has not completed + Message() string +} + +type result struct { + done bool + message string + err error +} + +func (r result) Done() bool { + return r.done +} + +func (r result) Message() string { + return r.message +} + +func (r result) Error() error { + return r.err +} + +// Continue returns a Result that indicates to WaitOn that it should continue +// polling. The message text will be used as the failure message if the timeout +// is reached. +func Continue(message string, args ...interface{}) Result { + return result{message: fmt.Sprintf(message, args...)} +} + +// Success returns a Result where Done() returns true, which indicates to WaitOn +// that it should stop polling and exit without an error. +func Success() Result { + return result{done: true} +} + +// Error returns a Result that indicates to WaitOn that it should fail the test +// and stop polling. +func Error(err error) Result { + return result{err: err} +} + +// WaitOn a condition or until a timeout. Poll by calling check and exit when +// check returns a done Result. To fail a test and exit polling with an error +// return a error result. +func WaitOn(t TestingT, check Check, pollOps ...SettingOp) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + config := defaultConfig() + for _, pollOp := range pollOps { + pollOp(config) + } + + var lastMessage string + after := time.After(config.Timeout) + chResult := make(chan Result) + for { + go func() { + chResult <- check(t) + }() + select { + case <-after: + if lastMessage == "" { + lastMessage = "first check never completed" + } + t.Fatalf("timeout hit after %s: %s", config.Timeout, lastMessage) + case result := <-chResult: + switch { + case result.Error() != nil: + t.Fatalf("polling check failed: %s", result.Error()) + case result.Done(): + return + } + time.Sleep(config.Delay) + lastMessage = result.Message() + } + } +} + +// Compare values using the cmp.Comparison. If the comparison fails return a +// result which indicates to WaitOn that it should continue waiting. +// If the comparison is successful then WaitOn stops polling. +func Compare(compare cmp.Comparison) Result { + buf := new(logBuffer) + if assert.RunComparison(buf, assert.ArgsAtZeroIndex, compare) { + return Success() + } + return Continue(buf.String()) +} + +type logBuffer struct { + log [][]interface{} +} + +func (c *logBuffer) Log(args ...interface{}) { + c.log = append(c.log, args) +} + +func (c *logBuffer) String() string { + b := new(strings.Builder) + for _, item := range c.log { + b.WriteString(fmt.Sprint(item...) + " ") + } + return b.String() +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/poll_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/poll_test.go new file mode 100644 index 0000000..36bd457 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/poll/poll_test.go @@ -0,0 +1,87 @@ +package poll + +import ( + "fmt" + "testing" + "time" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +type fakeT struct { + failed string +} + +func (t *fakeT) Fatalf(format string, args ...interface{}) { + t.failed = fmt.Sprintf(format, args...) + panic("exit wait on") +} + +func (t *fakeT) Log(args ...interface{}) {} + +func (t *fakeT) Logf(format string, args ...interface{}) {} + +func TestWaitOn(t *testing.T) { + counter := 0 + end := 4 + check := func(t LogT) Result { + if counter == end { + return Success() + } + counter++ + return Continue("counter is at %d not yet %d", counter-1, end) + } + + WaitOn(t, check, WithDelay(0)) + assert.Equal(t, end, counter) +} + +func TestWaitOnWithTimeout(t *testing.T) { + fakeT := &fakeT{} + + check := func(t LogT) Result { + return Continue("not done") + } + + assert.Assert(t, cmp.Panics(func() { + WaitOn(fakeT, check, WithTimeout(time.Millisecond)) + })) + assert.Equal(t, "timeout hit after 1ms: not done", fakeT.failed) +} + +func TestWaitOnWithCheckTimeout(t *testing.T) { + fakeT := &fakeT{} + + check := func(t LogT) Result { + time.Sleep(1 * time.Second) + return Continue("not done") + } + + assert.Assert(t, cmp.Panics(func() { WaitOn(fakeT, check, WithTimeout(time.Millisecond)) })) + assert.Equal(t, "timeout hit after 1ms: first check never completed", fakeT.failed) +} + +func TestWaitOnWithCheckError(t *testing.T) { + fakeT := &fakeT{} + + check := func(t LogT) Result { + return Error(fmt.Errorf("broke")) + } + + assert.Assert(t, cmp.Panics(func() { WaitOn(fakeT, check) })) + assert.Equal(t, "polling check failed: broke", fakeT.failed) +} + +func TestWaitOn_WithCompare(t *testing.T) { + fakeT := &fakeT{} + + check := func(t LogT) Result { + return Compare(cmp.Equal(3, 4)) + } + + assert.Assert(t, cmp.Panics(func() { + WaitOn(fakeT, check, WithDelay(0), WithTimeout(10*time.Millisecond)) + })) + assert.Assert(t, cmp.Contains(fakeT.failed, "assertion failed: 3 (int) != 4 (int)")) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/scripts/binary-gty-migrate-from-testify b/root/pkg/mod/gotest.tools/v3@v3.3.0/scripts/binary-gty-migrate-from-testify new file mode 100644 index 0000000..cb79fdd --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/scripts/binary-gty-migrate-from-testify @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec go build -o ./dist/gty-migrate-from-testify ./assert/cmd/gty-migrate-from-testify diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/scripts/test-unit b/root/pkg/mod/gotest.tools/v3@v3.3.0/scripts/test-unit new file mode 100644 index 0000000..0ed48f0 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/scripts/test-unit @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +paths=${@:-$(go list ./... | grep -v '/vendor/')} +TESTFLAGS=${TESTFLAGS-} +TESTTIMEOUT=${TESTTIMEOUT-30s} +go test -test.timeout "$TESTTIMEOUT" $TESTFLAGS -v $paths diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/example_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/example_test.go new file mode 100644 index 0000000..31c2148 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/example_test.go @@ -0,0 +1,43 @@ +package skip_test + +import ( + "testing" + + "gotest.tools/v3/skip" +) + +var apiVersion = "" + +type env struct{} + +func (e env) hasFeature(_ string) bool { return false } + +var testEnv = env{} + +func MissingFeature() bool { return false } + +var t = &testing.T{} + +func ExampleIf() { + // --- SKIP: TestName (0.00s) + // skip.go:19: MissingFeature + skip.If(t, MissingFeature) + + // --- SKIP: TestName (0.00s) + // skip.go:19: MissingFeature: coming soon + skip.If(t, MissingFeature, "coming soon") +} + +func ExampleIf_withExpression() { + // --- SKIP: TestName (0.00s) + // skip.go:19: apiVersion < version("v1.24") + skip.If(t, apiVersion < version("v1.24")) + + // --- SKIP: TestName (0.00s) + // skip.go:19: !textenv.hasFeature("build"): coming soon + skip.If(t, !testEnv.hasFeature("build"), "coming soon") +} + +func version(v string) string { + return v +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/skip.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/skip.go new file mode 100644 index 0000000..cb899f7 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/skip.go @@ -0,0 +1,89 @@ +/*Package skip provides functions for skipping a test and printing the source code +of the condition used to skip the test. +*/ +package skip // import "gotest.tools/v3/skip" + +import ( + "fmt" + "path" + "reflect" + "runtime" + "strings" + + "gotest.tools/v3/internal/format" + "gotest.tools/v3/internal/source" +) + +type skipT interface { + Skip(args ...interface{}) + Log(args ...interface{}) +} + +// Result of skip function +type Result interface { + Skip() bool + Message() string +} + +type helperT interface { + Helper() +} + +// BoolOrCheckFunc can be a bool, func() bool, or func() Result. Other types will panic +type BoolOrCheckFunc interface{} + +// If the condition expression evaluates to true, skip the test. +// +// The condition argument may be one of three types: bool, func() bool, or +// func() SkipResult. +// When called with a bool, the test will be skip if the condition evaluates to true. +// When called with a func() bool, the test will be skip if the function returns true. +// When called with a func() Result, the test will be skip if the Skip method +// of the result returns true. +// The skip message will contain the source code of the expression. +// Extra message text can be passed as a format string with args. +func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + switch check := condition.(type) { + case bool: + ifCondition(t, check, msgAndArgs...) + case func() bool: + if check() { + t.Skip(format.WithCustomMessage(getFunctionName(check), msgAndArgs...)) + } + case func() Result: + result := check() + if result.Skip() { + msg := getFunctionName(check) + ": " + result.Message() + t.Skip(format.WithCustomMessage(msg, msgAndArgs...)) + } + default: + panic(fmt.Sprintf("invalid type for condition arg: %T", check)) + } +} + +func getFunctionName(function interface{}) string { + funcPath := runtime.FuncForPC(reflect.ValueOf(function).Pointer()).Name() + return strings.SplitN(path.Base(funcPath), ".", 2)[1] +} + +func ifCondition(t skipT, condition bool, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !condition { + return + } + const ( + stackIndex = 2 + argPos = 1 + ) + source, err := source.FormattedCallExprArg(stackIndex, argPos) + if err != nil { + t.Log(err.Error()) + t.Skip(format.Message(msgAndArgs...)) + } + t.Skip(format.WithCustomMessage(source, msgAndArgs...)) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/skip_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/skip_test.go new file mode 100644 index 0000000..49b4dda --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/skip/skip_test.go @@ -0,0 +1,136 @@ +package skip + +import ( + "bytes" + "fmt" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +type fakeSkipT struct { + reason string + logs []string +} + +func (f *fakeSkipT) Skip(args ...interface{}) { + buf := new(bytes.Buffer) + for _, arg := range args { + buf.WriteString(fmt.Sprintf("%s", arg)) + } + f.reason = buf.String() +} + +func (f *fakeSkipT) Log(args ...interface{}) { + f.logs = append(f.logs, fmt.Sprintf("%s", args[0])) +} + +func (f *fakeSkipT) Helper() {} + +func version(v string) string { + return v +} + +func TestIfCondition(t *testing.T) { + skipT := &fakeSkipT{} + apiVersion := "v1.4" + If(skipT, apiVersion < version("v1.6")) + + assert.Equal(t, `apiVersion < version("v1.6")`, skipT.reason) + assert.Assert(t, cmp.Len(skipT.logs, 0)) +} + +func TestIfConditionWithMessage(t *testing.T) { + skipT := &fakeSkipT{} + apiVersion := "v1.4" + If(skipT, apiVersion < "v1.6", "see notes") + + assert.Equal(t, `apiVersion < "v1.6": see notes`, skipT.reason) + assert.Assert(t, cmp.Len(skipT.logs, 0)) +} + +func TestIfConditionMultiline(t *testing.T) { + skipT := &fakeSkipT{} + apiVersion := "v1.4" + If( + skipT, + apiVersion < "v1.6") + + assert.Equal(t, `apiVersion < "v1.6"`, skipT.reason) + assert.Assert(t, cmp.Len(skipT.logs, 0)) +} + +func TestIfConditionMultilineWithMessage(t *testing.T) { + skipT := &fakeSkipT{} + apiVersion := "v1.4" + If( + skipT, + apiVersion < "v1.6", + "see notes") + + assert.Equal(t, `apiVersion < "v1.6": see notes`, skipT.reason) + assert.Assert(t, cmp.Len(skipT.logs, 0)) +} + +func TestIfConditionNoSkip(t *testing.T) { + skipT := &fakeSkipT{} + If(skipT, false) + + assert.Equal(t, "", skipT.reason) + assert.Assert(t, cmp.Len(skipT.logs, 0)) +} + +func SkipBecauseISaidSo() bool { + return true +} + +func TestIf(t *testing.T) { + skipT := &fakeSkipT{} + If(skipT, SkipBecauseISaidSo) + + assert.Equal(t, "SkipBecauseISaidSo", skipT.reason) +} + +func TestIfWithMessage(t *testing.T) { + skipT := &fakeSkipT{} + If(skipT, SkipBecauseISaidSo, "see notes") + + assert.Equal(t, "SkipBecauseISaidSo: see notes", skipT.reason) +} + +func TestIf_InvalidCondition(t *testing.T) { + skipT := &fakeSkipT{} + assert.Assert(t, cmp.Panics(func() { + If(skipT, "just a string") + })) +} + +func TestIfWithSkipResultFunc(t *testing.T) { + t.Run("no extra message", func(t *testing.T) { + skipT := &fakeSkipT{} + If(skipT, alwaysSkipWithMessage) + + assert.Equal(t, "alwaysSkipWithMessage: skip because I said so!", skipT.reason) + }) + t.Run("with extra message", func(t *testing.T) { + skipT := &fakeSkipT{} + If(skipT, alwaysSkipWithMessage, "also %v", 4) + + assert.Equal(t, "alwaysSkipWithMessage: skip because I said so!: also 4", skipT.reason) + }) +} + +func alwaysSkipWithMessage() Result { + return skipResult{} +} + +type skipResult struct{} + +func (s skipResult) Skip() bool { + return true +} + +func (s skipResult) Message() string { + return "skip because I said so!" +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/x/doc.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/x/doc.go new file mode 100644 index 0000000..90f4541 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/x/doc.go @@ -0,0 +1,5 @@ +/*Package x is a namespace for experimental packages. Packages under x have looser +compatibility requirements. Packages in this namespace may contain backwards +incompatible changes within the same major version. +*/ +package x // import "gotest.tools/v3/x" diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/context.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/context.go new file mode 100644 index 0000000..708d940 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/context.go @@ -0,0 +1,93 @@ +/*Package subtest provides a TestContext to subtests which handles cleanup, and +provides a testing.TB, and context.Context. + +This package was inspired by github.com/frankban/quicktest. + +DEPRECATED + +With the addition of T.Cleanup() in go1.14 this package provides very +little value. A context.Context can be managed by tests that need it with +little enough boilerplate that it doesn't make sense to wrap testing.T in a +TestContext. +*/ +package subtest // import "gotest.tools/v3/x/subtest" + +import ( + "context" + "testing" + + "gotest.tools/v3/internal/cleanup" +) + +type testcase struct { + testing.TB + ctx context.Context + cleanupFuncs []cleanupFunc +} + +type cleanupFunc func() + +func (tc *testcase) Ctx() context.Context { + if tc.ctx == nil { + var cancel func() + tc.ctx, cancel = context.WithCancel(context.Background()) + cleanup.Cleanup(tc, cancel) + } + return tc.ctx +} + +// cleanup runs all cleanup functions. Functions are run in the opposite order +// in which they were added. Cleanup is called automatically before Run exits. +func (tc *testcase) cleanup() { + for _, f := range tc.cleanupFuncs { + // Defer all cleanup functions so they all run even if one calls + // t.FailNow() or panics. Deferring them also runs them in reverse order. + defer f() + } + tc.cleanupFuncs = nil +} + +func (tc *testcase) AddCleanup(f func()) { + tc.cleanupFuncs = append(tc.cleanupFuncs, f) +} + +func (tc *testcase) Parallel() { + tp, ok := tc.TB.(parallel) + if !ok { + panic("Parallel called with a testing.B") + } + tp.Parallel() +} + +type parallel interface { + Parallel() +} + +// Run a subtest. When subtest exits, every cleanup function added with +// TestContext.AddCleanup will be run. +func Run(t *testing.T, name string, subtest func(t TestContext)) bool { + return t.Run(name, func(t *testing.T) { + tc := &testcase{TB: t} + defer tc.cleanup() + subtest(tc) + }) +} + +// TestContext provides a testing.TB and a context.Context for a test case. +type TestContext interface { + testing.TB + // AddCleanup function which will be run when before Run returns. + // + // Deprecated: Go 1.14+ now includes a testing.TB.Cleanup(func()) which + // should be used instead. AddCleanup will be removed in a future release. + AddCleanup(f func()) + // Ctx returns a context for the test case. Multiple calls from the same subtest + // will return the same context. The context is cancelled when Run + // returns. + Ctx() context.Context + // Parallel calls t.Parallel on the testing.TB. Panics if testing.TB does + // not implement Parallel. + Parallel() +} + +var _ TestContext = &testcase{} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/context_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/context_test.go new file mode 100644 index 0000000..56b316e --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/context_test.go @@ -0,0 +1,32 @@ +package subtest + +import ( + "context" + "testing" + + "gotest.tools/v3/assert" +) + +func TestTestcase_Run_CallsCleanup(t *testing.T) { + calls := []int{} + var ctx context.Context + Run(t, "test-run-cleanup", func(t TestContext) { + cleanup := func(n int) func() { + return func() { + calls = append(calls, n) + } + } + ctx = t.Ctx() + t.AddCleanup(cleanup(2)) + t.AddCleanup(cleanup(1)) + t.AddCleanup(cleanup(0)) + }) + assert.DeepEqual(t, calls, []int{0, 1, 2}) + assert.Equal(t, ctx.Err(), context.Canceled) +} + +func TestTestcase_Run_Parallel(t *testing.T) { + Run(t, "test-parallel", func(t TestContext) { + t.Parallel() + }) +} diff --git a/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/example_test.go b/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/example_test.go new file mode 100644 index 0000000..b4365e0 --- /dev/null +++ b/root/pkg/mod/gotest.tools/v3@v3.3.0/x/subtest/example_test.go @@ -0,0 +1,66 @@ +package subtest_test + +import ( + "io" + "net/http" + "strings" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/x/subtest" +) + +var t = &testing.T{} + +func ExampleRun_tableTest() { + var testcases = []struct { + data io.Reader + expected int + }{ + { + data: strings.NewReader("invalid input"), + expected: 400, + }, + { + data: strings.NewReader("valid input"), + expected: 200, + }, + } + + for _, tc := range testcases { + subtest.Run(t, "test-service-call", func(t subtest.TestContext) { + // startFakeService can shutdown using t.AddCleanup + url := startFakeService(t) + + req, err := http.NewRequest("POST", url, tc.data) + assert.NilError(t, err) + req = req.WithContext(t.Ctx()) + + client := newClient(t) + resp, err := client.Do(req) + assert.NilError(t, err) + assert.Equal(t, resp.StatusCode, tc.expected) + }) + } +} + +func startFakeService(t subtest.TestContext) string { + return "url" +} + +func newClient(_ subtest.TestContext) *http.Client { + return &http.Client{} +} + +func ExampleRun_testSuite() { + // do suite setup before subtests + + subtest.Run(t, "test-one", func(t subtest.TestContext) { + assert.Equal(t, 1, 1) + }) + subtest.Run(t, "test-two", func(t subtest.TestContext) { + assert.Equal(t, 2, 2) + }) + + // do suite teardown after subtests +} diff --git a/root/pkg/sumdb/sum.golang.org/latest b/root/pkg/sumdb/sum.golang.org/latest new file mode 100644 index 0000000..829c8df --- /dev/null +++ b/root/pkg/sumdb/sum.golang.org/latest @@ -0,0 +1,5 @@ +go.sum database tree +46687924 +oqyj00KFhQ1r/LMLYjS+N2ZcyMBmuy4E20Qx5wubH18= + +— sum.golang.org Az3grskfHYkRAlx6xDdD0Ld7PWz6CLORxgtPBxvd73HO8lvyC+Z2yYTTobhK5XRat+vJH6yqO8Oqxa4++MKlzqCl/QY= diff --git a/root/src/github.com/getyoti/age-scan-examples/go.sum b/root/src/github.com/getyoti/age-scan-examples/go.sum new file mode 100644 index 0000000..e553463 --- /dev/null +++ b/root/src/github.com/getyoti/age-scan-examples/go.sum @@ -0,0 +1,8 @@ +github.com/getyoti/yoti-go-sdk/v3 v3.14.0 h1:cMFC/PuN6kuxMfPwX4OkVyQDA5ZousWnVMfUhIhKZa0= +github.com/getyoti/yoti-go-sdk/v3 v3.14.0/go.mod h1:FH8g7mRttc6SBUd9P0Jihm7ut0rNhkU3rDFljUHL33I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=