api: add SNIHostName to Info #6407

Merged
merged 1 commit into from Oct 14, 2016

Conversation

Projects
None yet
5 participants
Owner

rogpeppe commented Oct 7, 2016

This means that we can have an arbitrary API addresses
in api.Info, including resolved IP addresses, but still connect
using the officially signed certificate to check. If there's
a private Juju CA cert available, we use that by preference
to make it possible to connect even if the server cannot
obtain an officially signed certificate.

api/apiclient.go
-var newWebsocketDialer = createWebsocketDialer
+// newWebsocketDialer is defined as a variable so that it can be overridden
+// for tests.
+var newWebsocketDialer = newWebsocketDialer0
@kat-co

kat-co Oct 7, 2016

Contributor

We need to instead take this in as a dependency to the call-chain.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

This is something that already exists. I would prefer not to have to fix everything just because it's been touched.

Also, as expressed in our chat online, this is very much an implementation detail - the tests that use this
are checking that some particular logic has been wired up correctly. It wouldn't make sense for external
code to swap out this particular implementation detail.

If we were to make this an external dependency, I'd be more inclined to make the dependency
a higher level thing (something that returns an rpc.Codec for example), but that's something that
would better suit a separate PR, I think.

@natefinch

natefinch Oct 7, 2016

Contributor

I don't think that's really necessary for this change. If this were all new code, it would be worth talking about, but this is really just a small change to existing code. Reworking how it does all this stuff is not a good use of our time, IMO.

@kat-co

kat-co Oct 7, 2016

Contributor

To address your comment, @natefinch: "broken windows principle". We must start addressing our technical debt, or risk being crushed by it. I think this is also a decision we've made: moving forward, we fix things as we touch them.

Also, exposing one new function which takes in a dialer would not be a very invasive change and would allow us to remove the patched global variables.

To address your comment, @rogpeppe: I agree, usually exposing these kinds of things reveals a higher-level abstraction which has been missing all along. Probably not worth adressing that in this PR, but it doesn't have to be all or nothing.

@rogpeppe

rogpeppe Oct 10, 2016

Owner

I got most of the way to doing a higher level abstraction, but it ended up quite intrusive because
we don't just dial websockets for a JSON codec - we also use them for streaming,
so the interface required grew until I couldn't really justify it in terms of cost/benefit.

If we want to change the websocket implementation, we'll be much better just doing it
internally - it really feels to me like an internal detail of the api package (which is
why I don't like exposing that implementation package as part of the public
API as I am now doing).

@rogpeppe

rogpeppe Oct 10, 2016

Owner

I think this is also a decision we've made: moving forward, we fix things as we touch them.

For me "touching" something doesn't just involve a mechanical change to that thing, but actually
involves changing something substantial about the code, otherwise you could say, for example,
that changing an import path "touched" every file that was changed and therefore a PR that
changes an import path needs to fix every file that uses that import.

I have been doing my best to fix any tech debt I've been encountering in code that I change - I
believe strongly in leaving code cleaner than I find it, but I also think that it's really important
to maintain focus in every PR as far as possible (and, yes, I do break that rule
sometimes, usually when rewinding the change to make it independent is going to take lots
of time).

c.Assert(err, jc.ErrorIsNil)
conn.Close()
- assertConnAddrForEnv(c, conn, proxy.Addr(), s.State.ModelUUID(), "/api")
+ assertConnAddrForModel(c, conn, proxy.Addr(), s.State.ModelUUID())
@kat-co

kat-co Oct 7, 2016

Contributor

Is ensuring the proxy can connect a precondition for the real test? If so, I don't think we need this check.

If not, we need to split this test into two.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

This is an existing test that I've just changed because I renamed some things. If we want to fix it, it would be better to do in another PR, if that's OK.

@kat-co

kat-co Oct 7, 2016

Contributor

Also going to claim "broken windows principle" here. I'm still not sure if it's a precondition or not, but either way, it would be a small change to remove that line or split the test into two.

@rogpeppe

rogpeppe Oct 10, 2016

Owner

No, really, let's please not make multiple independent changes in one PR just because the code happens to be visible in the PR. It's important that tests remain unchanged when possible so that we know that the change hasn't broken them (of course, it's not always possible to leave them unchanged, but the principle remains).

@kat-co

kat-co Oct 11, 2016

Contributor

Fair enough. Please create another PR with this comment addressed and ping me for review.

@@ -94,7 +95,7 @@ func (s *apiclientSuite) TestConnectWebsocketMultipleError(c *gc.C) {
info := s.APIInfo(c)
addr := listener.Addr().String()
info.Addrs = []string{addr, addr, addr}
- _, _, err = api.ConnectWebsocket(info, api.DialOpts{})
+ _, _, err = api.DialAPI(info, api.DialOpts{})
c.Assert(err, gc.ErrorMatches, `unable to connect to API: websocket.Dial wss://.*/model/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/api: .*`)
@kat-co

kat-co Oct 7, 2016

Contributor

Having just gone through fixing a bunch of tests because the error message is now prepended with something different, please modify this to only check the relevant portion of the error message.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

Again, this is existing code. I'd prefer to avoid mixing too many concerns in this PR.

And although I feel your pain (and I've had the same issue before), I have also had problems when error messages weren't checked properly. I think that, on balance, although it's a pain to change the error messages, it's also good to see in the tests what the actual message will look like (to some degree of accuracy), as that's often the only time you'll get to check that the user will see an appropriate message in an often-rare case.

That is, error messages are important and it's good to see what they actually look like. I've often made changes to error messages because I've asserted them in the tests and they look too awkward.

@kat-co

kat-co Oct 7, 2016

Contributor

If we feel it's important to check the entirety of the error message, that should be done in its own test so we don't repeat the check in every test and make our suite brittle.

@rogpeppe

rogpeppe Oct 10, 2016

Owner

That's probably a good idea, but I'm not in the business of fixing every other test with this issue in this PR. If I was fixing this, I'd also fix the other places that have a similar error message, and that may well make the PR considerably bigger and certainly less focused.

@kat-co

kat-co Oct 11, 2016

Contributor

For the record, I'm not requesting that you fix every test, just modify the error message in the test you're fixing. You mentioned wanting to check the entire error message, so I suggested a new test to check just that.

Pursuant to your previous comment about not wanting test-fixes in this PR, please open another PR with the test modified and ping me for review.

@rogpeppe

rogpeppe Oct 12, 2016

Owner

I'm not sure that a mechanical rename really counts as "fixing" a test. I did a global search and replace - I didn't cast my eye on this test for a moment. There's a class of problem you've identified: some error messages in tests are more specific than would be ideal. A decent way to fix that is not, in my view, to change an error message because it's visible in a particular code review, but to make a PR specifically to address the issue - that way the tests remain self-consistent. Otherwise someone coming in later might be confused as to which precedent to follow to remain consistent with the tests in the rest of the package. I think consistency is a worthy aim.

close(stopped)
result, err := f(stopped)
c.Assert(err, gc.Equals, parallel.ErrStopped)
c.Assert(result, gc.IsNil)
}
+func (s *apiclientSuite) TestOpenWithSNIHostNameEmptyCert(c *gc.C) {
@kat-co

kat-co Oct 7, 2016

Contributor

What is the expected behavior of this test? Should be in the name or at least a comment.

@kat-co

kat-co Oct 11, 2016

Contributor

Sorry, @rogpeppe. It's probably because I don't know how to use GitHub reviews yet, but did this name change? The name I see is TestOpenWithSNIHostNameEmptyCert which clearly enumerates what's being tested, but gives me no clue what behavior I should expect.

Same comment for the other tests.

@rogpeppe

rogpeppe Oct 12, 2016

Owner

You suggested "in the name or at least a comment". I added a comment (L170). I guess I could name the test TestOpenWithSNIHostNameEmptyCertAlwaysUsesSNIName, but I thought the comment should be ok.

@kat-co

kat-co Oct 12, 2016

Contributor

Oh, OK. Sorry, GitHub reviews make it difficult to see what's changed when commits are squashed.

Do you think the comment should by on the method and not within the test?

+ }})
+}
+
+func (s *apiclientSuite) TestOpenWithSNIHostNameWithCACert(c *gc.C) {
@kat-co

kat-co Oct 7, 2016

Contributor

What is the expected behavior of this test? Should be in the name or at least a comment.

api/apiclient_test.go
+// (one element for each call to newWebsocketDialer)
+func (s *apiclientSuite) testSNIHostName(c *gc.C, info *api.Info, expectDials []apiDialInfo) {
+ dialed := make(chan *websocket.Config)
+ s.PatchValue(api.NewWebsocketDialer, func(cfg *websocket.Config, opts api.DialOpts) func(<-chan struct{}) (io.Closer, error) {
@kat-co

kat-co Oct 7, 2016

Contributor

As mentioned earlier, we need to get rid of the patching.

api/apiclient_test.go
+ c.Check(conn, gc.Equals, nil)
+ c.Check(err, gc.ErrorMatches, `nope`)
+ }()
+ for _, expect := range expectDials {
@kat-co

kat-co Oct 7, 2016

Contributor

I think we can make this test a little easier to read through by inverting what you're doing here:

  • Create a timeout channel that your fake NewWebsocketDialer can close over (or somehow use the one we're ignoring on L212 -- actually, why isn't that bubbling up to the API we're interacting with?).
  • In your anonymous goroutine, do sleep for jtesting.LongWait and then close the timeout channel (or maybe just fail the test outright?). This is semantically different in that we have one timeout for all sends, but 10*time.Second should be long enough for all sends.
  • Invert this for loop to range over dialed

This way I think you can get rid of both select blocks and we're looking at a simple for loop.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

Won't that cause the test to take 10 seconds to run?
Also, there are (at least) two goroutines in play - they can't both close the timeout channel.

Perhaps I've misunderstood though - could you explain with some (pseudo)code?

@kat-co

kat-co Oct 7, 2016

Contributor

Sure, sorry it wasn't more clear:

func (s *apiclientSuite) testSNIHostName(c *gc.C, info *api.Info, expectDials []apiDialInfo) {
    timeout := time.After(jtesting.LongWait)
    dialed := make(chan *websocket.Config)

    fakeDialer := func(cfg *websocket.Config, opts api.DialOpts) func(<-chan struct{}) (io.Closer, error) {
        return func(<-chan struct{}) (io.Closer, error) {
            select {
            case <-timeout:
                // Consider panicing... error doesn't make it all the
                // way up to returning from api.Open.
                return nil, errors.New("timeout")
            case dialed <- cfg:
            }
            return nil, errors.New("nope")
        }
    }
    s.PatchValue(api.NewWebsocketDialer, fakeDialer)
    go func() {
        defer close(dialed)
        conn, err := api.Open(info, api.DialOpts{})
        c.Check(conn, jc.ErrorIsNil)
        c.Check(err, gc.ErrorMatches, `nope`)
    }()

    i := 0
    for cfg := range dialed {
        c.Check(cfg.Location.String(), gc.Equals, expectDials[i].location)
        c.Assert(cfg.TlsConfig, gc.NotNil)
        c.Check(cfg.TlsConfig.RootCAs != nil, gc.Equals, expectDials[i].hasRootCAs)
        c.Check(cfg.TlsConfig.ServerName, gc.Equals, expectDials[i].serverName)
        i++
    }

    c.Check(i, gc.Equals, len(expectDials))
}
@rogpeppe

rogpeppe Oct 8, 2016

Owner

Ah I see now, clever, thanks! We know we can close the channel after api.Open returns because each attempt returns an error so we can be sure that they're all tried before api.Open completes.
I don't think the timeout channel is necessary at all - if things go wrong enough that the function can't send on dialed, we'll get a panic not a timeout because the channel will have been closed.

Then we end up with something like this, which is definitely much nicer, thanks again.

func (s *apiclientSuite) testSNIHostName(c *gc.C, info *api.Info, expectDials []apiDialInfo) {
    dialed := make(chan *websocket.Config)
    fakeDialer := func(cfg *websocket.Config, opts api.DialOpts) func(<-chan struct{}) (io.Closer, error) {
        return func(<-chan struct{}) (io.Closer, error) {
            dialed <- cfg
            return nil, errors.New("nope")
        }
    }
    s.PatchValue(api.NewWebsocketDialer, fakeDialer)
    go func() {
        conn, err := api.Open(info, api.DialOpts{})
        c.Check(conn, jc.ErrorIsNil)
        c.Check(err, gc.ErrorMatches, `nope`)
        // Because all the dials return an error, we're sure that
        // all the addresses will have been tried (and the values
        // sent on dialed) before api.Open returns, so it's safe
        // to close the channel now.
        close(dialed)
    }()

    i := 0
    for cfg := range dialed {
        expect := expectDials[i]
        c.Check(cfg.Location.String(), gc.Equals, expect.location)
        c.Assert(cfg.TlsConfig, gc.NotNil)
        c.Check(cfg.TlsConfig.RootCAs != nil, gc.Equals, expect.hasRootCAs)
        c.Check(cfg.TlsConfig.ServerName, gc.Equals, expect.serverName)
        i++
    }

    c.Check(i, gc.Equals, len(expectDials))
}
@rogpeppe

rogpeppe Oct 10, 2016

Owner

After changing the test and making it work, I'm no longer so sure it's better.
I think the new version is more brittle - it presupposes more about the internals
of the code being tested and will fail in a not-very-nice way (panic or hang forwever) if that
doesn't behave as expected.

I think the new version imposes somewhat more cognitive load, as the reader needs
to figure out why it's OK to close the channel.

The old version is longer, it's true, but has a more obvious failure mode - every
receive is guarded by a select so it's immediately clear that the test can't hang
forever, and there's no implicit ordering constraint between the call to the dial
function and the return from api.Open.

I think I'd prefer to stick with the original if that's OK.

@kat-co

kat-co Oct 11, 2016

Contributor

I think with my version it won't hang forever as the timeout is for the entire test, not each send, and panics in tests don't bother me one bit :)

Which way you go is definitely up to you, but FWIW I think the clean-reading test is worth it! I just find the multiple select statements to obfuscate the intent of the test.

api/apiclient_test.go
+ c.Assert(cfg.Location.String(), gc.Equals, expect.location)
+ c.Assert(cfg.TlsConfig, gc.NotNil)
+ c.Assert(cfg.TlsConfig.RootCAs != nil, gc.Equals, expect.hasRootCAs)
+ c.Assert(cfg.TlsConfig.ServerName, gc.Equals, expect.serverName)
@kat-co

kat-co Oct 7, 2016

Contributor

I think we can change these to c.Check.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

L228 still needs to be an Assert but otherwise, done.

Thanks for the review, @kat-co. I have a few comments in response.

api/apiclient.go
-var newWebsocketDialer = createWebsocketDialer
+// newWebsocketDialer is defined as a variable so that it can be overridden
+// for tests.
+var newWebsocketDialer = newWebsocketDialer0
@kat-co

kat-co Oct 7, 2016

Contributor

We need to instead take this in as a dependency to the call-chain.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

This is something that already exists. I would prefer not to have to fix everything just because it's been touched.

Also, as expressed in our chat online, this is very much an implementation detail - the tests that use this
are checking that some particular logic has been wired up correctly. It wouldn't make sense for external
code to swap out this particular implementation detail.

If we were to make this an external dependency, I'd be more inclined to make the dependency
a higher level thing (something that returns an rpc.Codec for example), but that's something that
would better suit a separate PR, I think.

@natefinch

natefinch Oct 7, 2016

Contributor

I don't think that's really necessary for this change. If this were all new code, it would be worth talking about, but this is really just a small change to existing code. Reworking how it does all this stuff is not a good use of our time, IMO.

@kat-co

kat-co Oct 7, 2016

Contributor

To address your comment, @natefinch: "broken windows principle". We must start addressing our technical debt, or risk being crushed by it. I think this is also a decision we've made: moving forward, we fix things as we touch them.

Also, exposing one new function which takes in a dialer would not be a very invasive change and would allow us to remove the patched global variables.

To address your comment, @rogpeppe: I agree, usually exposing these kinds of things reveals a higher-level abstraction which has been missing all along. Probably not worth adressing that in this PR, but it doesn't have to be all or nothing.

@rogpeppe

rogpeppe Oct 10, 2016

Owner

I got most of the way to doing a higher level abstraction, but it ended up quite intrusive because
we don't just dial websockets for a JSON codec - we also use them for streaming,
so the interface required grew until I couldn't really justify it in terms of cost/benefit.

If we want to change the websocket implementation, we'll be much better just doing it
internally - it really feels to me like an internal detail of the api package (which is
why I don't like exposing that implementation package as part of the public
API as I am now doing).

@rogpeppe

rogpeppe Oct 10, 2016

Owner

I think this is also a decision we've made: moving forward, we fix things as we touch them.

For me "touching" something doesn't just involve a mechanical change to that thing, but actually
involves changing something substantial about the code, otherwise you could say, for example,
that changing an import path "touched" every file that was changed and therefore a PR that
changes an import path needs to fix every file that uses that import.

I have been doing my best to fix any tech debt I've been encountering in code that I change - I
believe strongly in leaving code cleaner than I find it, but I also think that it's really important
to maintain focus in every PR as far as possible (and, yes, I do break that rule
sometimes, usually when rewinding the change to make it independent is going to take lots
of time).

c.Assert(err, jc.ErrorIsNil)
conn.Close()
- assertConnAddrForEnv(c, conn, proxy.Addr(), s.State.ModelUUID(), "/api")
+ assertConnAddrForModel(c, conn, proxy.Addr(), s.State.ModelUUID())
@kat-co

kat-co Oct 7, 2016

Contributor

Is ensuring the proxy can connect a precondition for the real test? If so, I don't think we need this check.

If not, we need to split this test into two.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

This is an existing test that I've just changed because I renamed some things. If we want to fix it, it would be better to do in another PR, if that's OK.

@kat-co

kat-co Oct 7, 2016

Contributor

Also going to claim "broken windows principle" here. I'm still not sure if it's a precondition or not, but either way, it would be a small change to remove that line or split the test into two.

@rogpeppe

rogpeppe Oct 10, 2016

Owner

No, really, let's please not make multiple independent changes in one PR just because the code happens to be visible in the PR. It's important that tests remain unchanged when possible so that we know that the change hasn't broken them (of course, it's not always possible to leave them unchanged, but the principle remains).

@kat-co

kat-co Oct 11, 2016

Contributor

Fair enough. Please create another PR with this comment addressed and ping me for review.

@@ -94,7 +95,7 @@ func (s *apiclientSuite) TestConnectWebsocketMultipleError(c *gc.C) {
info := s.APIInfo(c)
addr := listener.Addr().String()
info.Addrs = []string{addr, addr, addr}
- _, _, err = api.ConnectWebsocket(info, api.DialOpts{})
+ _, _, err = api.DialAPI(info, api.DialOpts{})
c.Assert(err, gc.ErrorMatches, `unable to connect to API: websocket.Dial wss://.*/model/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/api: .*`)
@kat-co

kat-co Oct 7, 2016

Contributor

Having just gone through fixing a bunch of tests because the error message is now prepended with something different, please modify this to only check the relevant portion of the error message.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

Again, this is existing code. I'd prefer to avoid mixing too many concerns in this PR.

And although I feel your pain (and I've had the same issue before), I have also had problems when error messages weren't checked properly. I think that, on balance, although it's a pain to change the error messages, it's also good to see in the tests what the actual message will look like (to some degree of accuracy), as that's often the only time you'll get to check that the user will see an appropriate message in an often-rare case.

That is, error messages are important and it's good to see what they actually look like. I've often made changes to error messages because I've asserted them in the tests and they look too awkward.

@kat-co

kat-co Oct 7, 2016

Contributor

If we feel it's important to check the entirety of the error message, that should be done in its own test so we don't repeat the check in every test and make our suite brittle.

@rogpeppe

rogpeppe Oct 10, 2016

Owner

That's probably a good idea, but I'm not in the business of fixing every other test with this issue in this PR. If I was fixing this, I'd also fix the other places that have a similar error message, and that may well make the PR considerably bigger and certainly less focused.

@kat-co

kat-co Oct 11, 2016

Contributor

For the record, I'm not requesting that you fix every test, just modify the error message in the test you're fixing. You mentioned wanting to check the entire error message, so I suggested a new test to check just that.

Pursuant to your previous comment about not wanting test-fixes in this PR, please open another PR with the test modified and ping me for review.

@rogpeppe

rogpeppe Oct 12, 2016

Owner

I'm not sure that a mechanical rename really counts as "fixing" a test. I did a global search and replace - I didn't cast my eye on this test for a moment. There's a class of problem you've identified: some error messages in tests are more specific than would be ideal. A decent way to fix that is not, in my view, to change an error message because it's visible in a particular code review, but to make a PR specifically to address the issue - that way the tests remain self-consistent. Otherwise someone coming in later might be confused as to which precedent to follow to remain consistent with the tests in the rest of the package. I think consistency is a worthy aim.

api/apiclient_test.go
+ c.Check(conn, gc.Equals, nil)
+ c.Check(err, gc.ErrorMatches, `nope`)
+ }()
+ for _, expect := range expectDials {
@kat-co

kat-co Oct 7, 2016

Contributor

I think we can make this test a little easier to read through by inverting what you're doing here:

  • Create a timeout channel that your fake NewWebsocketDialer can close over (or somehow use the one we're ignoring on L212 -- actually, why isn't that bubbling up to the API we're interacting with?).
  • In your anonymous goroutine, do sleep for jtesting.LongWait and then close the timeout channel (or maybe just fail the test outright?). This is semantically different in that we have one timeout for all sends, but 10*time.Second should be long enough for all sends.
  • Invert this for loop to range over dialed

This way I think you can get rid of both select blocks and we're looking at a simple for loop.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

Won't that cause the test to take 10 seconds to run?
Also, there are (at least) two goroutines in play - they can't both close the timeout channel.

Perhaps I've misunderstood though - could you explain with some (pseudo)code?

@kat-co

kat-co Oct 7, 2016

Contributor

Sure, sorry it wasn't more clear:

func (s *apiclientSuite) testSNIHostName(c *gc.C, info *api.Info, expectDials []apiDialInfo) {
    timeout := time.After(jtesting.LongWait)
    dialed := make(chan *websocket.Config)

    fakeDialer := func(cfg *websocket.Config, opts api.DialOpts) func(<-chan struct{}) (io.Closer, error) {
        return func(<-chan struct{}) (io.Closer, error) {
            select {
            case <-timeout:
                // Consider panicing... error doesn't make it all the
                // way up to returning from api.Open.
                return nil, errors.New("timeout")
            case dialed <- cfg:
            }
            return nil, errors.New("nope")
        }
    }
    s.PatchValue(api.NewWebsocketDialer, fakeDialer)
    go func() {
        defer close(dialed)
        conn, err := api.Open(info, api.DialOpts{})
        c.Check(conn, jc.ErrorIsNil)
        c.Check(err, gc.ErrorMatches, `nope`)
    }()

    i := 0
    for cfg := range dialed {
        c.Check(cfg.Location.String(), gc.Equals, expectDials[i].location)
        c.Assert(cfg.TlsConfig, gc.NotNil)
        c.Check(cfg.TlsConfig.RootCAs != nil, gc.Equals, expectDials[i].hasRootCAs)
        c.Check(cfg.TlsConfig.ServerName, gc.Equals, expectDials[i].serverName)
        i++
    }

    c.Check(i, gc.Equals, len(expectDials))
}
@rogpeppe

rogpeppe Oct 8, 2016

Owner

Ah I see now, clever, thanks! We know we can close the channel after api.Open returns because each attempt returns an error so we can be sure that they're all tried before api.Open completes.
I don't think the timeout channel is necessary at all - if things go wrong enough that the function can't send on dialed, we'll get a panic not a timeout because the channel will have been closed.

Then we end up with something like this, which is definitely much nicer, thanks again.

func (s *apiclientSuite) testSNIHostName(c *gc.C, info *api.Info, expectDials []apiDialInfo) {
    dialed := make(chan *websocket.Config)
    fakeDialer := func(cfg *websocket.Config, opts api.DialOpts) func(<-chan struct{}) (io.Closer, error) {
        return func(<-chan struct{}) (io.Closer, error) {
            dialed <- cfg
            return nil, errors.New("nope")
        }
    }
    s.PatchValue(api.NewWebsocketDialer, fakeDialer)
    go func() {
        conn, err := api.Open(info, api.DialOpts{})
        c.Check(conn, jc.ErrorIsNil)
        c.Check(err, gc.ErrorMatches, `nope`)
        // Because all the dials return an error, we're sure that
        // all the addresses will have been tried (and the values
        // sent on dialed) before api.Open returns, so it's safe
        // to close the channel now.
        close(dialed)
    }()

    i := 0
    for cfg := range dialed {
        expect := expectDials[i]
        c.Check(cfg.Location.String(), gc.Equals, expect.location)
        c.Assert(cfg.TlsConfig, gc.NotNil)
        c.Check(cfg.TlsConfig.RootCAs != nil, gc.Equals, expect.hasRootCAs)
        c.Check(cfg.TlsConfig.ServerName, gc.Equals, expect.serverName)
        i++
    }

    c.Check(i, gc.Equals, len(expectDials))
}
@rogpeppe

rogpeppe Oct 10, 2016

Owner

After changing the test and making it work, I'm no longer so sure it's better.
I think the new version is more brittle - it presupposes more about the internals
of the code being tested and will fail in a not-very-nice way (panic or hang forwever) if that
doesn't behave as expected.

I think the new version imposes somewhat more cognitive load, as the reader needs
to figure out why it's OK to close the channel.

The old version is longer, it's true, but has a more obvious failure mode - every
receive is guarded by a select so it's immediately clear that the test can't hang
forever, and there's no implicit ordering constraint between the call to the dial
function and the return from api.Open.

I think I'd prefer to stick with the original if that's OK.

@kat-co

kat-co Oct 11, 2016

Contributor

I think with my version it won't hang forever as the timeout is for the entire test, not each send, and panics in tests don't bother me one bit :)

Which way you go is definitely up to you, but FWIW I think the clean-reading test is worth it! I just find the multiple select statements to obfuscate the intent of the test.

api/apiclient_test.go
+ c.Assert(cfg.Location.String(), gc.Equals, expect.location)
+ c.Assert(cfg.TlsConfig, gc.NotNil)
+ c.Assert(cfg.TlsConfig.RootCAs != nil, gc.Equals, expect.hasRootCAs)
+ c.Assert(cfg.TlsConfig.ServerName, gc.Equals, expect.serverName)
@kat-co

kat-co Oct 7, 2016

Contributor

I think we can change these to c.Check.

@rogpeppe

rogpeppe Oct 7, 2016

Owner

L228 still needs to be an Assert but otherwise, done.

LGTM

👍

Contributor

kat-co commented Oct 11, 2016

Thanks @rogpeppe! I think this is almost ready to land. I just had a few follow up questions.

And thanks for waiting for 2x people on core to +1; very considerate!

@rogpeppe rogpeppe changed the base branch from master to develop Oct 14, 2016

api: add SNIHostName to Info
This means that we can have an arbitrary API addresses
in api.Info, including resolved IP addresses, but still connect
using the officially signed certificate to check. If there's
a private Juju CA cert available, we use that by preference
to make it possible to connect even if the server cannot
obtain an officially signed certificate.

We also add a DialWebsocket field to api.DialOpts
so that we can use a fake dialer function in tests.
Owner

rogpeppe commented Oct 14, 2016

$$merge$$

Contributor

jujubot commented Oct 14, 2016

Status: merge request accepted. Url: http://juju-ci.vapour.ws:8080/job/github-merge-juju

@jujubot jujubot merged commit 30d9f27 into juju:develop Oct 14, 2016

1 check passed

github-check-merge-juju Built PR, ran unit tests, and tested LXD deploy.
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment