github.com/ericwq/aprilsh@v0.0.0-20240517091432-958bc568daa0/frontend/server/server_linux_test.go (about)

     1  // Copyright 2022~2023 wangqi. All rights reserved.
     2  // Use of this source code is governed by a MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build linux
     6  
     7  package main
     8  
     9  import (
    10  	"os"
    11  	"reflect"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/creack/pty"
    16  	"github.com/ericwq/aprilsh/util"
    17  	utmps "github.com/ericwq/goutmp"
    18  )
    19  
    20  var strENOTTY = "inappropriate ioctl for device"
    21  
    22  var (
    23  	index         int
    24  	utmpxMockData []*utmps.Utmpx
    25  )
    26  
    27  type mockData struct {
    28  	xtype int
    29  	host  string
    30  	line  string
    31  	usr   string
    32  	id    int
    33  	pid   int
    34  }
    35  
    36  func initData(data []mockData) {
    37  	utmpxMockData = make([]*utmps.Utmpx, 0)
    38  	for _, v := range data {
    39  		u := &utmps.Utmpx{}
    40  		u.SetType(v.xtype)
    41  		u.SetHost(v.host)
    42  		u.SetLine(v.line)
    43  		u.SetUser(v.usr)
    44  		u.SetId(v.id)
    45  		u.SetPid(v.pid)
    46  		utmpxMockData = append(utmpxMockData, u)
    47  	}
    48  }
    49  
    50  // return utmp mock data
    51  func mockGetRecord() *utmps.Utmpx {
    52  	if 0 <= index && index < len(utmpxMockData) {
    53  		p := utmpxMockData[index]
    54  		index++
    55  		return p
    56  	}
    57  	// fmt.Println("mockGetRecord return nil")
    58  	return nil
    59  }
    60  
    61  func TestWarnUnattached(t *testing.T) {
    62  	tc := []struct {
    63  		label      string
    64  		ignoreHost string
    65  		count      int
    66  	}{
    67  		// 666 pts/0 exist, 888 pts/7 does not exist, only 666 remains
    68  		{"one match", "apshd:999", 1},
    69  		// 666 pts0 exist, 999 pts/ptmx exist, so 666 and 999 remains
    70  		{"two match", "apshd:888", 2},
    71  	}
    72  
    73  	data := []mockData{
    74  		{utmps.USER_PROCESS, "apshd:777", "pts/1", "root", 3, 1},
    75  		{utmps.USER_PROCESS, "apshd:888", "pts/7", getCurrentUser(), 7, 1221},
    76  		{utmps.USER_PROCESS, "apshd:666", "pts/0", getCurrentUser(), 0, 1222},
    77  		{utmps.USER_PROCESS, "192.168.0.123 via apshd:555", "pts/0", getCurrentUser(), 0, 1223},
    78  		{utmps.USER_PROCESS, "apshd:999", "pts/ptmx", getCurrentUser(), 2, 1224},
    79  	}
    80  	initData(data)
    81  
    82  	// open pts for test,
    83  	// on some container, if pts/0 is not opened, the test will failed. so we open it.
    84  	ptmx, pts, err := pty.Open()
    85  	if err != nil {
    86  		t.Errorf("test warnUnattached open pts failed, %s\n", err)
    87  	}
    88  	defer func() {
    89  		ptmx.Close()
    90  		pts.Close()
    91  	}()
    92  
    93  	for _, v := range tc {
    94  		t.Run(v.label, func(t *testing.T) {
    95  			var out strings.Builder
    96  			setGetRecord(mockGetRecord)
    97  			index = 0
    98  			defer func() {
    99  				setGetRecord(utmps.GetRecord)
   100  			}()
   101  
   102  			warnUnattached(&out, getCurrentUser(), v.ignoreHost)
   103  
   104  			got := out.String()
   105  			// t.Logf("%q\n", got)
   106  			count := strings.Count(got, "- ")
   107  			switch count {
   108  			case 0: // warnUnattached found one unattached session
   109  				if strings.Index(got, "a detached session on this server") != -1 &&
   110  					v.count != 1 {
   111  					t.Errorf("#test warnUnattached() %q expect %d warning, got 1.\n",
   112  						v.label, v.count)
   113  				}
   114  			default: // warnUnattached found more than one unattached session
   115  				if count != v.count {
   116  					t.Errorf("#test warnUnattached() %q expect %d warning, got %d. \n%s\n",
   117  						v.label, v.count, count, got)
   118  				}
   119  			}
   120  		})
   121  	}
   122  }
   123  
   124  func TestWarnUnattachedZero(t *testing.T) {
   125  	tc := []struct {
   126  		label      string
   127  		ignoreHost string
   128  		count      int
   129  	}{
   130  		{"zero match", "apshd:888", 0},
   131  	}
   132  
   133  	data := []mockData{
   134  		{utmps.USER_PROCESS, "apshd:777", "pts/1", "root", 3, 1},
   135  		{utmps.USER_PROCESS, "192.168.0.123 via apshd:888", "pts/8", getCurrentUser(), 7, 1221},
   136  		{utmps.USER_PROCESS, "192.168.0.123 via apshd:666", "pts/0", getCurrentUser(), 0, 1222},
   137  		{utmps.USER_PROCESS, "192.168.0.123 via apshd:555", "pts/9", getCurrentUser(), 0, 1223},
   138  		{utmps.USER_PROCESS, "192.168.0.123 via apshd:999", "pts/ptmx", getCurrentUser(), 2, 1224},
   139  	}
   140  	initData(data)
   141  	for _, v := range tc {
   142  		t.Run(v.label, func(t *testing.T) {
   143  			var out strings.Builder
   144  			setGetRecord(mockGetRecord)
   145  			index = 0
   146  			defer func() {
   147  				setGetRecord(utmps.GetRecord)
   148  			}()
   149  
   150  			warnUnattached(&out, getCurrentUser(), v.ignoreHost)
   151  
   152  			got := out.String()
   153  			// t.Logf("%q\n", got)
   154  			if len(got) != 0 {
   155  				t.Errorf("#test %q expect empty string, got %q\n", v.label, got)
   156  			}
   157  		})
   158  	}
   159  }
   160  
   161  func TestBuildConfig(t *testing.T) {
   162  	tc := []struct {
   163  		label string
   164  		conf0 Config
   165  		conf2 Config
   166  		hint  string
   167  		ok    bool
   168  	}{
   169  		{
   170  			"UTF-8 locale",
   171  			Config{
   172  				version: false, server: false, verbose: 0, desiredIP: "", desiredPort: "",
   173  				locales:     localeFlag{"LC_ALL": "en_US.UTF-8", "LANG": "en_US.UTF-8"},
   174  				commandPath: "", commandArgv: []string{"/bin/sh", "-sh"}, withMotd: false,
   175  			},
   176  			Config{
   177  				version: false, server: false, verbose: 0, desiredIP: "", desiredPort: "",
   178  				locales:     localeFlag{"LC_ALL": "en_US.UTF-8", "LANG": "en_US.UTF-8"},
   179  				commandPath: "/bin/sh", commandArgv: []string{"-sh"}, withMotd: false,
   180  			},
   181  			"", true,
   182  		},
   183  		{
   184  			"empty commandArgv",
   185  			Config{
   186  				version: false, server: false, verbose: 0, desiredIP: "", desiredPort: "",
   187  				locales:     localeFlag{"LC_ALL": "en_US.UTF-8"},
   188  				commandPath: "", commandArgv: []string{}, withMotd: false,
   189  			},
   190  			Config{
   191  				version: false, server: false, verbose: 0, desiredIP: "", desiredPort: "",
   192  				locales:     localeFlag{"LC_ALL": "en_US.UTF-8"},
   193  				commandPath: "/bin/sh", commandArgv: []string{"-sh"}, withMotd: true,
   194  				flowControl: _FC_DEF_BASH_SHELL,
   195  			},
   196  			// macOS: /bin/zsh
   197  			// alpine: /bin/ash
   198  			"", true,
   199  		},
   200  		// {
   201  		// 	"non UTF-8 locale",
   202  		// 	Config{
   203  		// 		version: false, server: false, verbose: 0, desiredIP: "", desiredPort: "",
   204  		// 		locales: localeFlag{"LC_ALL": "zh_CN.GB2312", "LANG": "zh_CN.GB2312"},
   205  		// 		commandPath: "", commandArgv: []string{"/bin/sh", "-sh"}, withMotd: false,
   206  		// 	}, // TODO GB2312 is not available in apline linux
   207  		// 	Config{
   208  		// 		version: false, server: false, verbose: 0, desiredIP: "", desiredPort: "",
   209  		// 		locales: localeFlag{},
   210  		// 		commandPath: "/bin/sh", commandArgv: []string{"*sh"}, withMotd: false,
   211  		// 	},
   212  		// 	errors.New("UTF-8 locale fail."),
   213  		// },
   214  		{
   215  			"commandArgv is one string",
   216  			Config{
   217  				version: false, server: false, verbose: 0, desiredIP: "", desiredPort: "",
   218  				locales:     localeFlag{"LC_ALL": "en_US.UTF-8", "LANG": "en_US.UTF-8"},
   219  				commandPath: "", commandArgv: []string{"/bin/sh"}, withMotd: false,
   220  			},
   221  			Config{
   222  				version: false, server: false, verbose: 0, desiredIP: "", desiredPort: "",
   223  				locales:     localeFlag{"LC_ALL": "en_US.UTF-8", "LANG": "en_US.UTF-8"},
   224  				commandPath: "/bin/sh", commandArgv: []string{"-sh"}, withMotd: false,
   225  			},
   226  			"", true,
   227  		},
   228  		{
   229  			"missing SSH_CONNECTION",
   230  			Config{
   231  				version: false, server: true, verbose: 0, desiredIP: "", desiredPort: "",
   232  				locales:     localeFlag{"LC_ALL": "en_US.UTF-8", "LANG": "en_US.UTF-8"},
   233  				commandPath: "", commandArgv: []string{"/bin/sh", "-sh"}, withMotd: false,
   234  			},
   235  			Config{
   236  				version: false, server: true, verbose: 0, desiredIP: "", desiredPort: "",
   237  				locales:     localeFlag{"LC_ALL": "en_US.UTF-8", "LANG": "en_US.UTF-8"},
   238  				commandPath: "", commandArgv: []string{"/bin/sh", "-sh"}, withMotd: false,
   239  			},
   240  			"Warning: SSH_CONNECTION not found; binding to any interface.", false,
   241  		},
   242  	}
   243  
   244  	for _, v := range tc {
   245  		t.Run(v.label, func(t *testing.T) {
   246  
   247  			// set SHELL for empty commandArgv
   248  			if len(v.conf0.commandArgv) == 0 {
   249  				shell := os.Getenv("SHELL")
   250  				defer os.Setenv("SHELL", shell)
   251  				os.Unsetenv("SHELL")
   252  
   253  				// getShell() will fail
   254  				defer func() {
   255  					v.conf0.flowControl = 0
   256  				}()
   257  
   258  				v.conf0.flowControl = _FC_DEF_BASH_SHELL
   259  			}
   260  
   261  			if v.conf0.server { // unset SSH_CONNECTION, getSSHip will return false
   262  				shell := os.Getenv("SSH_CONNECTION")
   263  				defer os.Setenv("SSH_CONNECTION", shell)
   264  				os.Unsetenv("SSH_CONNECTION")
   265  			}
   266  
   267  			// validate buildConfig
   268  			hint, ok := v.conf0.buildConfig()
   269  			v.conf0.serve = nil // disable the serve func for testing
   270  
   271  			if hint != v.hint || ok != v.ok {
   272  				t.Errorf("#test buildConfig got hint=%s, ok=%t, expect hint=%s, ok=%t\n", hint, ok, v.hint, v.ok)
   273  			}
   274  			if !reflect.DeepEqual(v.conf0, v.conf2) {
   275  				t.Errorf("#test buildConfig got \n%+v, expect \n%+v\n", v.conf0, v.conf2)
   276  			}
   277  			// reset the environment
   278  			util.ClearLocaleVariables()
   279  
   280  			// restore logW
   281  			// logW = log.New(os.Stdout, "WARN: ", log.Ldate|log.Ltime|log.Lshortfile)
   282  		})
   283  	}
   284  }