// 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.

// Tests for ssh client multi-auth
//
// These tests run a simple go ssh client against OpenSSH server
// over unix domain sockets. The tests use multiple combinations
// of password, keyboard-interactive and publickey authentication
// methods.
//
// A wrapper library for making sshd PAM authentication use test
// passwords is required in ./sshd_test_pw.so. If the library does
// not exist these tests will be skipped. See compile instructions
// (for linux) in file ./sshd_test_pw.c.

//go:build linux

package test

import (
	"fmt"
	"strings"
	"testing"

	"golang.org/x/crypto/ssh"
)

// test cases
type multiAuthTestCase struct {
	authMethods         []string
	expectedPasswordCbs int
	expectedKbdIntCbs   int
}

// test context
type multiAuthTestCtx struct {
	password       string
	numPasswordCbs int
	numKbdIntCbs   int
}

// create test context
func newMultiAuthTestCtx(t *testing.T) *multiAuthTestCtx {
	password, err := randomPassword()
	if err != nil {
		t.Fatalf("Failed to generate random test password: %s", err.Error())
	}

	return &multiAuthTestCtx{
		password: password,
	}
}

// password callback
func (ctx *multiAuthTestCtx) passwordCb() (secret string, err error) {
	ctx.numPasswordCbs++
	return ctx.password, nil
}

// keyboard-interactive callback
func (ctx *multiAuthTestCtx) kbdIntCb(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
	if len(questions) == 0 {
		return nil, nil
	}

	ctx.numKbdIntCbs++
	if len(questions) == 1 {
		return []string{ctx.password}, nil
	}

	return nil, fmt.Errorf("unsupported keyboard-interactive flow")
}

// TestMultiAuth runs several subtests for different combinations of password, keyboard-interactive and publickey authentication methods
func TestMultiAuth(t *testing.T) {
	testCases := []multiAuthTestCase{
		// Test password,publickey authentication, assert that password callback is called 1 time
		{
			authMethods:         []string{"password", "publickey"},
			expectedPasswordCbs: 1,
		},
		// Test keyboard-interactive,publickey authentication, assert that keyboard-interactive callback is called 1 time
		{
			authMethods:       []string{"keyboard-interactive", "publickey"},
			expectedKbdIntCbs: 1,
		},
		// Test publickey,password authentication, assert that password callback is called 1 time
		{
			authMethods:         []string{"publickey", "password"},
			expectedPasswordCbs: 1,
		},
		// Test publickey,keyboard-interactive authentication, assert that keyboard-interactive callback is called 1 time
		{
			authMethods:       []string{"publickey", "keyboard-interactive"},
			expectedKbdIntCbs: 1,
		},
		// Test password,password authentication, assert that password callback is called 2 times
		{
			authMethods:         []string{"password", "password"},
			expectedPasswordCbs: 2,
		},
	}

	for _, testCase := range testCases {
		t.Run(strings.Join(testCase.authMethods, ","), func(t *testing.T) {
			ctx := newMultiAuthTestCtx(t)

			server := newServerForConfig(t, "MultiAuth", map[string]string{"AuthMethods": strings.Join(testCase.authMethods, ",")})

			clientConfig := clientConfig()
			server.setTestPassword(clientConfig.User, ctx.password)

			publicKeyAuthMethod := clientConfig.Auth[0]
			clientConfig.Auth = nil
			for _, authMethod := range testCase.authMethods {
				switch authMethod {
				case "publickey":
					clientConfig.Auth = append(clientConfig.Auth, publicKeyAuthMethod)
				case "password":
					clientConfig.Auth = append(clientConfig.Auth,
						ssh.RetryableAuthMethod(ssh.PasswordCallback(ctx.passwordCb), 5))
				case "keyboard-interactive":
					clientConfig.Auth = append(clientConfig.Auth,
						ssh.RetryableAuthMethod(ssh.KeyboardInteractive(ctx.kbdIntCb), 5))
				default:
					t.Fatalf("Unknown authentication method %s", authMethod)
				}
			}

			conn := server.Dial(clientConfig)
			defer conn.Close()

			if ctx.numPasswordCbs != testCase.expectedPasswordCbs {
				t.Fatalf("passwordCallback was called %d times, expected %d times", ctx.numPasswordCbs, testCase.expectedPasswordCbs)
			}

			if ctx.numKbdIntCbs != testCase.expectedKbdIntCbs {
				t.Fatalf("keyboardInteractiveCallback was called %d times, expected %d times", ctx.numKbdIntCbs, testCase.expectedKbdIntCbs)
			}
		})
	}
}
