Fantom

 

//
// Copyright (c) 2011, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//    8 Jul 11  Brian Frank  Creation
//    20 Apr 16  Steve Krytkowski HTTPS Update
//

using concurrent

**
** WebRepoTest
**
class WebRepoTest : Test
{
  internal TestWebRepoAuth auth := TestWebRepoAuth("bob", "123")
  internal Service? wispService
  internal Int httpPort := 1972
  internal Int? httpsPort := null

  internal Repo? pub      // client with no username, password
  internal Repo? badUser  // client with bad username
  internal Repo? badPass  // client with bad password
  internal Repo? good     // client with proper authentication

  PodSpec? webSpec   // set in doVerifyQuery

//////////////////////////////////////////////////////////////////////////
// Setup/Teardown
//////////////////////////////////////////////////////////////////////////

  override Void setup()
  {
    // create pod file repo
    fr := FileRepo(tempDir.uri)
    fr.publish(podFile("web"))
    fr.publish(podFile("wisp"))
    fr.publish(podFile("util")) // no one allowed to query

    // wrap with WebRepoMod
    mod := WebRepoMod
    {
      it.repo = fr
      it.auth = this.auth
      it.pingMeta = it.pingMeta.dup.add("extra", "foo")
    }

    // open wisp service on test port
    wispService = WebRepoMain.makeWispService(mod, httpPort, httpsPort)
    wispService->log->level = LogLevel.silent
    wispService.start

    // setup clients
    pub     = Repo.makeForUri(`http://localhost:$httpPort/`)
    good    = Repo.makeForUri(`http://localhost:$httpPort/`, "bob", "123")
    badUser = Repo.makeForUri(`http://localhost:$httpPort/`, "bad", "123")
    badPass = Repo.makeForUri(`http://localhost:$httpPort/`, "bob", "bad")
  }

  override Void teardown()
  {
    wispService?.uninstall
  }

  File podFile(Str podName) { Env.cur.homeDir + `lib/fan/${podName}.pod` }

//////////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////////

  Void test()
  {
    verifyPing
    verifyFind
    verifyQuery
    verifyRead
    verifyPublish
  }

//////////////////////////////////////////////////////////////////////////
// Ping
//////////////////////////////////////////////////////////////////////////

  Void verifyPing()
  {
    // bad credentials
    verifyBadCredentials |r| { r.ping }

    // public and login have access to ping
    doVerifyPing(pub)
    doVerifyPing(good)
  }

  Void doVerifyPing(Repo r)
  {
    p := r.ping
    verifyEq(p["fanr.version"], typeof.pod.version.toStr)
    verifyEq(p["fanr.type"], WebRepo#.qname)
    verifyEq(p["extra"], "foo")
    verifyEq(DateTime.fromStr(p["ts"]).date, Date.today)
  }

//////////////////////////////////////////////////////////////////////////
// Find
//////////////////////////////////////////////////////////////////////////

  Void verifyFind()
  {
    // bad credentials
    verifyBadCredentials |r| { r.find("foo", Version("1.0")) }

    // public not allowed
    auth.allowPublic.val = false
    verifyAuthRequired |r| { r.find("foo", Version("1.0")) }

    // public allowed
    auth.allowPublic.val = true
    doVerifyFind(pub)

    // login not allowed
    auth.allowUser.val = auth.allowPublic.val = false
    verifyForbidden |r| { r.find("foo", Version("1.0")) }

    // login allowed
    auth.allowUser.val = true
    doVerifyFind(good)
  }

  Void doVerifyFind(Repo r)
  {
    wisp := Pod.find("wisp")  // allowed
    util := Pod.find("util")  // never allowed

    pod := r.find("wisp", wisp.version)
    verifyEq(pod.name, "wisp")
    verifyEq(pod.version, wisp.version)

    pod = r.find("wisp", null)
    verifyEq(pod.name, "wisp")
    verifyEq(pod.version, wisp.version)

    badVer := Version("28.99.1234")
    verifyEq(r.find("fooBarNotFound", Version("1.0.123"), false), null)
    verifyEq(r.find("wisp", badVer, false), null)
    verifyErr(UnknownPodErr#) { r.find("wisp", badVer) }
    verifyErr(UnknownPodErr#) { r.find("wisp", badVer, true) }

    verifyForbidden |x| { x.find("util", util.version) }
  }

//////////////////////////////////////////////////////////////////////////
// Query
//////////////////////////////////////////////////////////////////////////

  Void verifyQuery()
  {
    // bad credentials
    verifyBadCredentials |r| { r.query("*") }

    // public not allowed
    auth.allowPublic.val = false
    verifyAuthRequired |r| { r.query("*") }

    // public allowed
    auth.allowPublic.val = true
    doVerifyQuery(pub)

    // login not allowed
    auth.allowUser.val = auth.allowPublic.val = false
    verifyForbidden |r| { r.query("*") }

    // login allowed
    auth.allowUser.val = true
    doVerifyQuery(good)
  }

  Void doVerifyQuery(Repo r)
  {
    pods := r.query("*").sort
    verifyEq(pods.size, 2)
    verifyEq(pods[0].name, "web")
    verifyEq(pods[1].name, "wisp")

    webSpec = pods[0]
  }

//////////////////////////////////////////////////////////////////////////
// Read
//////////////////////////////////////////////////////////////////////////

  Void verifyRead()
  {
    // bad credentials
    verifyBadCredentials |r| { r.read(webSpec) }

    // public not allowed
    auth.allowPublic.val = false
    verifyAuthRequired |r| { r.read(webSpec) }

    // public allowed
    auth.allowPublic.val = true
    doVerifyRead(pub)

    // login not allowed
    auth.allowUser.val = auth.allowPublic.val = false
    verifyForbidden |r| { r.read(webSpec) }

    // login allowed
    auth.allowUser.val = true
    doVerifyRead(good)
  }

  Void doVerifyRead(Repo r)
  {
    temp := tempDir + `web-download.pod`
    out := temp.out
    r.read(webSpec).pipe(out)
    out.close

    spec := PodSpec.load(temp)
    verifyEq(spec.name, "web")
    verifyEq(spec.meta["org.name"], "Fantom")
  }

//////////////////////////////////////////////////////////////////////////
// Publish
//////////////////////////////////////////////////////////////////////////

  Void verifyPublish()
  {
    f := podFile("inet")
    // bad credentials
    verifyBadCredentials |r| { r.publish(f) }

    // public not allowed
    auth.allowPublic.val = false
    verifyAuthRequired |r| { r.publish(f) }

    // public allowed
    auth.allowPublic.val = true
    doVerifyPublish(pub, f)

    // login not allowed
    f = podFile("build")
    auth.allowUser.val = auth.allowPublic.val = false
    verifyForbidden |r| { r.publish(f) }

    // login allowed
    auth.allowUser.val = true
    doVerifyPublish(good, f)

    // no one allowed to publish util
    f = podFile("util")
    verifyForbidden |r| { r.publish(f) }

    // verify query that we successfully published two new pods
    pods := good.query("*").sort
    verifyEq(pods.size, 4)
    verifyEq(pods[0].name, "build")
    verifyEq(pods[1].name, "inet")
    verifyEq(pods[2].name, "web")
    verifyEq(pods[3].name, "wisp")
  }

  Void doVerifyPublish(Repo r, File f)
  {
    spec := r.publish(f)
    verifyEq(spec.name, f.basename)
  }

//////////////////////////////////////////////////////////////////////////
// Authentication
//////////////////////////////////////////////////////////////////////////

  ** Test with invalid username and invalid password
  Void verifyBadCredentials (|Repo| f)
  {
    verifyAuthErr("Invalid username: bad [401]", badUser, f)
    verifyAuthErr("Invalid password (invalid signature) [401]", badPass, f)
  }

  ** Test that public (no credentials) reports auth required
  Void verifyAuthRequired(|Repo| f)
  {
    verifyAuthErr("Authentication required [401]", pub, f)
  }

  ** Test that valid login account is forbidden from an operation
  Void verifyForbidden(|Repo| f)
  {
    verifyAuthErr("Not allowed [403]", good, f)
  }

  Void verifyAuthErr(Str msg, Repo r, |Repo| f)
  {
    Err? err := null
    try
      f(r)
    catch (Err e)
      err = e

    if (err == null) fail("No err raised: $msg")
    if (err isnot RemoteErr) err.trace

    verifyEq(err.typeof, RemoteErr#)
    // echo("     $err")
    verifyEq(err.msg, msg)
  }

}

internal const class TestWebRepoAuth : SimpleWebRepoAuth
{
  new make(Str u, Str p) : super(u, p) {}

  override Bool allowQuery(Obj? u, PodSpec? p) { allow(u, p) }
  override Bool allowRead(Obj? u, PodSpec? p) { allow(u, p) }
  override Bool allowPublish(Obj? u, PodSpec? p) { allow(u, p) }

  const AtomicBool allowPublic := AtomicBool(false)
  const AtomicBool allowUser   := AtomicBool(false)

  Bool allow(Obj? u, PodSpec? p)
  {
    if (p?.name == "util") return false // util never allowed
    return allowPublic.val  || (u != null && allowUser.val)
  }
}